OSDN Git Service

graphqlエンドポイント使用時にツイート検索の言語指定が効かない不具合を修正
[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 bool enabled = false;
43         private DateTimeUtc lastCall;
44         private bool calledSinceLastInvoke;
45         private bool refreshTimerEnabled;
46
47         public bool Enabled
48         {
49             get => this.enabled;
50             set
51             {
52                 if (value == this.enabled)
53                     return;
54
55                 this.enabled = value;
56
57                 if (!value)
58                     this.debouncingTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
59             }
60         }
61
62         public TimeSpan Interval { get; }
63
64         public bool InvokeLeading { get; }
65
66         public bool InvokeTrailing { get; }
67
68         public DebounceTimer(Func<Task> timerCallback, TimeSpan interval, bool leading, bool trailing)
69         {
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;
78         }
79
80         protected virtual ITimer CreateTimer(Func<Task> callback)
81             => new AsyncTimer(callback);
82
83         public async Task Call()
84         {
85             if (!this.Enabled)
86                 return;
87
88             bool startTimer, invoke;
89             lock (this.lockObject)
90             {
91                 this.lastCall = DateTimeUtc.Now;
92                 this.calledSinceLastInvoke = true;
93                 if (this.refreshTimerEnabled)
94                 {
95                     startTimer = false;
96                     invoke = false;
97                 }
98                 else
99                 {
100                     startTimer = true;
101                     invoke = this.InvokeLeading;
102                     this.refreshTimerEnabled = true;
103                 }
104             }
105
106             if (startTimer)
107             {
108                 if (invoke)
109                     await this.Invoke().ConfigureAwait(false);
110
111                 this.debouncingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
112             }
113         }
114
115         private async Task Execute()
116         {
117             bool startTimer, invoke;
118             TimeSpan wait;
119             lock (this.lockObject)
120             {
121                 var sinceLastCall = DateTimeUtc.Now - this.lastCall;
122
123                 if (sinceLastCall < TimeSpan.Zero)
124                 {
125                     // システムの時計が過去の時刻に変更された場合は無限ループを防ぐために lastCall をリセットする
126                     this.lastCall = DateTimeUtc.Now;
127                     sinceLastCall = TimeSpan.Zero;
128                 }
129
130                 if (sinceLastCall < this.Interval)
131                 {
132                     startTimer = true;
133                     wait = this.Interval - sinceLastCall;
134                     invoke = false;
135                 }
136                 else
137                 {
138                     startTimer = false;
139                     wait = TimeSpan.Zero;
140                     invoke = this.InvokeTrailing && this.calledSinceLastInvoke;
141                     this.refreshTimerEnabled = false;
142                 }
143             }
144
145             if (invoke)
146                 await this.Invoke().ConfigureAwait(false);
147
148             if (startTimer)
149                 this.debouncingTimer.Change(dueTime: wait, period: Timeout.InfiniteTimeSpan);
150         }
151
152         private async Task Invoke()
153         {
154             await Task.Run(async () =>
155             {
156                 lock (this.lockObject)
157                     this.calledSinceLastInvoke = false;
158
159                 await this.timerCallback().ConfigureAwait(false);
160             });
161         }
162
163         public void Dispose()
164             => this.debouncingTimer.Dispose();
165
166         public static DebounceTimer Create(Func<Task> callback, TimeSpan wait, bool leading = false, bool trailing = true)
167             => new(callback, wait, leading, trailing);
168     }
169 }