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 bool enabled = false;
43 private DateTimeUtc lastCall;
44 private bool calledSinceLastInvoke;
45 private bool refreshTimerEnabled;
52 if (value == this.enabled)
58 this.debouncingTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
62 public TimeSpan Interval { get; }
64 public bool InvokeLeading { get; }
66 public bool InvokeTrailing { get; }
68 public DebounceTimer(Func<Task> timerCallback, TimeSpan interval, bool leading, bool trailing)
70 this.timerCallback = timerCallback;
71 this.Interval = interval;
72 this.InvokeLeading = leading;
73 this.InvokeTrailing = trailing;
74 this.debouncingTimer = this.CreateTimer(this.Execute);
75 this.lastCall = DateTimeUtc.MinValue;
76 this.calledSinceLastInvoke = false;
77 this.refreshTimerEnabled = false;
80 protected virtual ITimer CreateTimer(Func<Task> callback)
81 => new AsyncTimer(callback);
83 public async Task Call()
88 bool startTimer, invoke;
89 lock (this.lockObject)
91 this.lastCall = DateTimeUtc.Now;
92 this.calledSinceLastInvoke = true;
93 if (this.refreshTimerEnabled)
101 invoke = this.InvokeLeading;
102 this.refreshTimerEnabled = true;
109 await this.Invoke().ConfigureAwait(false);
111 this.debouncingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
115 private async Task Execute()
117 bool startTimer, invoke;
119 lock (this.lockObject)
121 var sinceLastCall = DateTimeUtc.Now - this.lastCall;
123 if (sinceLastCall < TimeSpan.Zero)
125 // システムの時計が過去の時刻に変更された場合は無限ループを防ぐために lastCall をリセットする
126 this.lastCall = DateTimeUtc.Now;
127 sinceLastCall = TimeSpan.Zero;
130 if (sinceLastCall < this.Interval)
133 wait = this.Interval - sinceLastCall;
139 wait = TimeSpan.Zero;
140 invoke = this.InvokeTrailing && this.calledSinceLastInvoke;
141 this.refreshTimerEnabled = false;
146 await this.Invoke().ConfigureAwait(false);
149 this.debouncingTimer.Change(dueTime: wait, period: Timeout.InfiniteTimeSpan);
152 private async Task Invoke()
154 await Task.Run(async () =>
156 lock (this.lockObject)
157 this.calledSinceLastInvoke = false;
159 await this.timerCallback().ConfigureAwait(false);
163 public void Dispose()
164 => this.debouncingTimer.Dispose();
166 public static DebounceTimer Create(Func<Task> callback, TimeSpan wait, bool leading = false, bool trailing = true)
167 => new(callback, wait, leading, trailing);