1 // OpenTween - Client of Twitter
2 // Copyright (c) 2022 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
17 // You should have received a copy of the GNU General public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
25 using System.Threading;
26 using System.Threading.Tasks;
31 /// コールバック先の関数を <see cref="Interval"/> 未満の頻度で呼ばないよう制御するタイマー
34 /// lodash の <c>_.debounce()</c> に相当する機能となっている
36 public class DebounceTimer : IDisposable
38 private readonly ITimer debouncingTimer;
39 private readonly Func<Task> timerCallback;
40 private readonly object lockObject = new();
42 private DateTimeUtc lastCall;
43 private bool calledSinceLastInvoke;
44 private bool refreshTimerEnabled;
46 public TimeSpan Interval { get; }
48 public bool InvokeLeading { get; }
50 public bool InvokeTrailing { get; }
52 public DebounceTimer(Func<Task> timerCallback, TimeSpan interval, bool leading, bool trailing)
54 this.timerCallback = timerCallback;
55 this.Interval = interval;
56 this.InvokeLeading = leading;
57 this.InvokeTrailing = trailing;
58 this.debouncingTimer = this.CreateTimer(this.Execute);
59 this.lastCall = DateTimeUtc.MinValue;
60 this.calledSinceLastInvoke = false;
61 this.refreshTimerEnabled = false;
64 protected virtual ITimer CreateTimer(Func<Task> callback)
65 => new AsyncTimer(callback);
67 public async Task Call()
69 bool startTimer, invoke;
70 lock (this.lockObject)
72 this.lastCall = DateTimeUtc.Now;
73 this.calledSinceLastInvoke = true;
74 if (this.refreshTimerEnabled)
82 invoke = this.InvokeLeading;
83 this.refreshTimerEnabled = true;
90 await this.Invoke().ConfigureAwait(false);
92 this.debouncingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
96 private async Task Execute()
98 bool startTimer, invoke;
100 lock (this.lockObject)
102 var sinceLastCall = DateTimeUtc.Now - this.lastCall;
104 if (sinceLastCall < TimeSpan.Zero)
106 // システムの時計が過去の時刻に変更された場合は無限ループを防ぐために lastCall をリセットする
107 this.lastCall = DateTimeUtc.Now;
108 sinceLastCall = TimeSpan.Zero;
111 if (sinceLastCall < this.Interval)
114 wait = this.Interval - sinceLastCall;
120 wait = TimeSpan.Zero;
121 invoke = this.InvokeTrailing && this.calledSinceLastInvoke;
122 this.refreshTimerEnabled = false;
127 await this.Invoke().ConfigureAwait(false);
130 this.debouncingTimer.Change(dueTime: wait, period: Timeout.InfiniteTimeSpan);
133 private async Task Invoke()
135 await Task.Run(async () =>
137 lock (this.lockObject)
138 this.calledSinceLastInvoke = false;
140 await this.timerCallback().ConfigureAwait(false);
144 public void Dispose()
145 => this.debouncingTimer.Dispose();
147 public static DebounceTimer Create(Func<Task> callback, TimeSpan wait, bool leading = false, bool trailing = true)
148 => new(callback, wait, leading, trailing);