OSDN Git Service

Merge pull request #240 from opentween/fix-playable-mark
[opentween/open-tween.git] / OpenTween / DebounceTimer.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2022 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
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)
10 // any later version.
11 //
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
15 // for more details.
16 //
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.
21
22 #nullable enable
23
24 using System;
25 using System.Threading;
26 using System.Threading.Tasks;
27
28 namespace OpenTween
29 {
30     /// <summary>
31     /// コールバック先の関数を <see cref="Interval"/> 未満の頻度で呼ばないよう制御するタイマー
32     /// </summary>
33     /// <remarks>
34     /// lodash の <c>_.debounce()</c> に相当する機能となっている
35     /// </remarks>
36     public class DebounceTimer : IDisposable
37     {
38         private readonly ITimer debouncingTimer;
39         private readonly Func<Task> timerCallback;
40         private readonly object lockObject = new();
41
42         private DateTimeUtc lastCall;
43         private bool calledSinceLastInvoke;
44         private bool refreshTimerEnabled;
45
46         public TimeSpan Interval { get; }
47
48         public bool InvokeLeading { get; }
49
50         public bool InvokeTrailing { get; }
51
52         public DebounceTimer(Func<Task> timerCallback, TimeSpan interval, bool leading, bool trailing)
53         {
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;
62         }
63
64         protected virtual ITimer CreateTimer(Func<Task> callback)
65             => new AsyncTimer(callback);
66
67         public async Task Call()
68         {
69             bool startTimer, invoke;
70             lock (this.lockObject)
71             {
72                 this.lastCall = DateTimeUtc.Now;
73                 this.calledSinceLastInvoke = true;
74                 if (this.refreshTimerEnabled)
75                 {
76                     startTimer = false;
77                     invoke = false;
78                 }
79                 else
80                 {
81                     startTimer = true;
82                     invoke = this.InvokeLeading;
83                     this.refreshTimerEnabled = true;
84                 }
85             }
86
87             if (startTimer)
88             {
89                 if (invoke)
90                     await this.Invoke().ConfigureAwait(false);
91
92                 this.debouncingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
93             }
94         }
95
96         private async Task Execute()
97         {
98             bool startTimer, invoke;
99             TimeSpan wait;
100             lock (this.lockObject)
101             {
102                 var sinceLastCall = DateTimeUtc.Now - this.lastCall;
103
104                 if (sinceLastCall < TimeSpan.Zero)
105                 {
106                     // システムの時計が過去の時刻に変更された場合は無限ループを防ぐために lastCall をリセットする
107                     this.lastCall = DateTimeUtc.Now;
108                     sinceLastCall = TimeSpan.Zero;
109                 }
110
111                 if (sinceLastCall < this.Interval)
112                 {
113                     startTimer = true;
114                     wait = this.Interval - sinceLastCall;
115                     invoke = false;
116                 }
117                 else
118                 {
119                     startTimer = false;
120                     wait = TimeSpan.Zero;
121                     invoke = this.InvokeTrailing && this.calledSinceLastInvoke;
122                     this.refreshTimerEnabled = false;
123                 }
124             }
125
126             if (invoke)
127                 await this.Invoke().ConfigureAwait(false);
128
129             if (startTimer)
130                 this.debouncingTimer.Change(dueTime: wait, period: Timeout.InfiniteTimeSpan);
131         }
132
133         private async Task Invoke()
134         {
135             await Task.Run(async () =>
136             {
137                 lock (this.lockObject)
138                     this.calledSinceLastInvoke = false;
139
140                 await this.timerCallback().ConfigureAwait(false);
141             });
142         }
143
144         public void Dispose()
145             => this.debouncingTimer.Dispose();
146
147         public static DebounceTimer Create(Func<Task> callback, TimeSpan wait, bool leading = false, bool trailing = true)
148             => new(callback, wait, leading, trailing);
149     }
150 }