OSDN Git Service

C# 8.0 のnull許容参照型を有効化
[opentween/open-tween.git] / OpenTween / ThrottlingTimer.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2018 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     public class ThrottlingTimer : IDisposable
34     {
35         private const int TIMER_DISABLED = 0;
36         private const int TIMER_ENABLED = 1;
37
38         private readonly Timer throttlingTimer;
39         private readonly Func<Task> timerCallback;
40
41         private long lastCalledTick;
42         private long lastInvokedTick;
43         private int refreshTimerEnabled = TIMER_DISABLED;
44
45         public TimeSpan Interval { get; }
46         public TimeSpan MaxWait { get; }
47         public bool InvokeLeading { get; }
48         public bool InvokeTrailing { get; }
49
50         private DateTimeUtc LastCalled
51         {
52             get => new DateTimeUtc(Interlocked.Read(ref this.lastCalledTick));
53             set => Interlocked.Exchange(ref this.lastCalledTick, value.UtcTicks);
54         }
55
56         private DateTimeUtc LastInvoked
57         {
58             get => new DateTimeUtc(Interlocked.Read(ref this.lastInvokedTick));
59             set => Interlocked.Exchange(ref this.lastInvokedTick, value.UtcTicks);
60         }
61
62         public ThrottlingTimer(Func<Task> timerCallback, TimeSpan interval, TimeSpan maxWait, bool leading, bool trailing)
63         {
64             this.timerCallback = timerCallback;
65             this.Interval = interval;
66             this.MaxWait = maxWait;
67             this.LastCalled = DateTimeUtc.MinValue;
68             this.LastInvoked = DateTimeUtc.MinValue;
69             this.InvokeLeading = leading;
70             this.InvokeTrailing = trailing;
71             this.throttlingTimer = new Timer(this.Execute);
72         }
73
74         public void Call()
75         {
76             this.LastCalled = DateTimeUtc.Now;
77
78             if (this.refreshTimerEnabled == TIMER_DISABLED)
79             {
80                 this.refreshTimerEnabled = TIMER_ENABLED;
81                 this.LastInvoked = DateTimeUtc.MinValue;
82                 _ = Task.Run(async () =>
83                 {
84                     if (this.InvokeLeading)
85                         await this.timerCallback().ConfigureAwait(false);
86
87                     this.throttlingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
88                 });
89             }
90         }
91
92         private async void Execute(object _)
93         {
94             var lastCalled = this.LastCalled;
95             var lastInvoked = this.LastInvoked;
96
97             var timerExpired = lastCalled < lastInvoked;
98             if (timerExpired)
99             {
100                 // 前回実行時より後に lastInvoked が更新されていなければタイマーを止める
101                 this.refreshTimerEnabled = TIMER_DISABLED;
102
103                 if (this.InvokeTrailing)
104                     await this.timerCallback().ConfigureAwait(false);
105             }
106             else
107             {
108                 var now = DateTimeUtc.Now;
109
110                 if ((now - lastInvoked) >= this.MaxWait)
111                     await this.timerCallback().ConfigureAwait(false);
112
113                 this.LastInvoked = now;
114
115                 // dueTime は Execute が呼ばれる度に再設定する (period は使用しない)
116                 // これにより timerCallback の実行に Interval 以上の時間が掛かっても重複して実行されることはなくなる
117                 lock (this.throttlingTimer)
118                     this.throttlingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
119             }
120         }
121
122         public void Dispose()
123             => this.throttlingTimer.Dispose();
124
125         // lodash.js の _.throttle, _.debounce 的な処理をしたかったメソッド群
126         public static ThrottlingTimer Throttle(Func<Task> callback, TimeSpan wait, bool leading = true, bool trailing = true)
127             => new ThrottlingTimer(callback, wait, maxWait: wait, leading, trailing);
128
129         public static ThrottlingTimer Debounce(Func<Task> callback, TimeSpan wait, bool leading = false, bool trailing = true)
130             => new ThrottlingTimer(callback, wait, maxWait: TimeSpan.MaxValue, leading, trailing);
131     }
132 }