// OpenTween - Client of Twitter
// Copyright (c) 2018 kim_upsilon (@kim_upsilon)
// All rights reserved.
//
// This file is part of OpenTween.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
// for more details.
//
// You should have received a copy of the GNU General public License along
// with this program. If not, see , or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
namespace OpenTween
{
///
/// コールバック先の関数を 未満の頻度で呼ばないよう制御するタイマー
///
public class ThrottlingTimer : IDisposable
{
private const int TIMER_DISABLED = 0;
private const int TIMER_ENABLED = 1;
private readonly Timer throttlingTimer;
private readonly Func timerCallback;
private long lastCalledTick;
private long lastInvokedTick;
private int refreshTimerEnabled = TIMER_DISABLED;
public TimeSpan Interval { get; }
public TimeSpan MaxWait { get; }
public bool InvokeLeading { get; }
public bool InvokeTrailing { get; }
private DateTimeUtc LastCalled
{
get => new DateTimeUtc(Interlocked.Read(ref this.lastCalledTick));
set => Interlocked.Exchange(ref this.lastCalledTick, value.UtcTicks);
}
private DateTimeUtc LastInvoked
{
get => new DateTimeUtc(Interlocked.Read(ref this.lastInvokedTick));
set => Interlocked.Exchange(ref this.lastInvokedTick, value.UtcTicks);
}
public ThrottlingTimer(Func timerCallback, TimeSpan interval, TimeSpan maxWait, bool leading, bool trailing)
{
this.timerCallback = timerCallback;
this.Interval = interval;
this.MaxWait = maxWait;
this.LastCalled = DateTimeUtc.MinValue;
this.LastInvoked = DateTimeUtc.MinValue;
this.InvokeLeading = leading;
this.InvokeTrailing = trailing;
this.throttlingTimer = new Timer(this.Execute);
}
public void Call()
{
this.LastCalled = DateTimeUtc.Now;
if (this.refreshTimerEnabled == TIMER_DISABLED)
{
this.refreshTimerEnabled = TIMER_ENABLED;
this.LastInvoked = DateTimeUtc.MinValue;
_ = Task.Run(async () =>
{
if (this.InvokeLeading)
await this.timerCallback().ConfigureAwait(false);
this.throttlingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
});
}
}
private async void Execute(object _)
{
var lastCalled = this.LastCalled;
var lastInvoked = this.LastInvoked;
var timerExpired = lastCalled < lastInvoked;
if (timerExpired)
{
// 前回実行時より後に lastInvoked が更新されていなければタイマーを止める
this.refreshTimerEnabled = TIMER_DISABLED;
if (this.InvokeTrailing)
await this.timerCallback().ConfigureAwait(false);
}
else
{
var now = DateTimeUtc.Now;
if ((now - lastInvoked) >= this.MaxWait)
await this.timerCallback().ConfigureAwait(false);
this.LastInvoked = now;
// dueTime は Execute が呼ばれる度に再設定する (period は使用しない)
// これにより timerCallback の実行に Interval 以上の時間が掛かっても重複して実行されることはなくなる
lock (this.throttlingTimer)
this.throttlingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan);
}
}
public void Dispose()
=> this.throttlingTimer.Dispose();
// lodash.js の _.throttle, _.debounce 的な処理をしたかったメソッド群
public static ThrottlingTimer Throttle(Func callback, TimeSpan wait, bool leading = true, bool trailing = true)
=> new ThrottlingTimer(callback, wait, maxWait: wait, leading, trailing);
public static ThrottlingTimer Debounce(Func callback, TimeSpan wait, bool leading = false, bool trailing = true)
=> new ThrottlingTimer(callback, wait, maxWait: TimeSpan.MaxValue, leading, trailing);
}
}