1 // OpenTween - Client of Twitter
2 // Copyright (c) 2019 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.Collections.Generic;
26 using System.Threading;
27 using System.Threading.Tasks;
31 public class TimelineScheduler
33 private readonly AsyncTimer timer;
35 private bool enabled = false;
36 private bool systemResumeMode = false;
37 private bool preventTimerUpdate = false;
44 if (this.enabled == value)
49 var now = DateTimeUtc.Now;
50 this.LastUpdateHome = now;
51 this.LastUpdateMention = now;
52 this.LastUpdateDm = now;
53 this.LastUpdatePublicSearch = now;
54 this.LastUpdateUser = now;
55 this.LastUpdateList = now;
56 this.LastUpdateConfig = now;
59 this.RefreshSchedule();
63 public DateTimeUtc LastUpdateHome { get; private set; } = DateTimeUtc.MinValue;
64 public DateTimeUtc LastUpdateMention { get; private set; } = DateTimeUtc.MinValue;
65 public DateTimeUtc LastUpdateDm { get; private set; } = DateTimeUtc.MinValue;
66 public DateTimeUtc LastUpdatePublicSearch { get; private set; } = DateTimeUtc.MinValue;
67 public DateTimeUtc LastUpdateUser { get; private set; } = DateTimeUtc.MinValue;
68 public DateTimeUtc LastUpdateList { get; private set; } = DateTimeUtc.MinValue;
69 public DateTimeUtc LastUpdateConfig { get; private set; } = DateTimeUtc.MinValue;
70 public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue;
72 public TimeSpan UpdateIntervalHome { get; set; } = Timeout.InfiniteTimeSpan;
73 public TimeSpan UpdateIntervalMention { get; set; } = Timeout.InfiniteTimeSpan;
74 public TimeSpan UpdateIntervalDm { get; set; } = Timeout.InfiniteTimeSpan;
75 public TimeSpan UpdateIntervalPublicSearch { get; set; } = Timeout.InfiniteTimeSpan;
76 public TimeSpan UpdateIntervalUser { get; set; } = Timeout.InfiniteTimeSpan;
77 public TimeSpan UpdateIntervalList { get; set; } = Timeout.InfiniteTimeSpan;
78 public TimeSpan UpdateIntervalConfig { get; set; } = Timeout.InfiniteTimeSpan;
79 public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan;
81 public bool EnableUpdateHome
82 => this.UpdateIntervalHome != Timeout.InfiniteTimeSpan;
84 public bool EnableUpdateMention
85 => this.UpdateIntervalMention != Timeout.InfiniteTimeSpan;
87 public bool EnableUpdateDm
88 => this.UpdateIntervalDm != Timeout.InfiniteTimeSpan;
90 public bool EnableUpdatePublicSearch
91 => this.UpdateIntervalPublicSearch != Timeout.InfiniteTimeSpan;
93 public bool EnableUpdateUser
94 => this.UpdateIntervalUser != Timeout.InfiniteTimeSpan;
96 public bool EnableUpdateList
97 => this.UpdateIntervalList != Timeout.InfiniteTimeSpan;
99 public bool EnableUpdateConfig
100 => this.UpdateIntervalConfig != Timeout.InfiniteTimeSpan;
102 public bool EnableUpdateSystemResume
103 => this.UpdateAfterSystemResume != Timeout.InfiniteTimeSpan;
105 public Func<Task>? UpdateHome;
106 public Func<Task>? UpdateMention;
107 public Func<Task>? UpdateDm;
108 public Func<Task>? UpdatePublicSearch;
109 public Func<Task>? UpdateUser;
110 public Func<Task>? UpdateList;
111 public Func<Task>? UpdateConfig;
114 private enum UpdateTask
120 PublicSearch = 1 << 4,
124 All = Home | Mention | Dm | PublicSearch | User | List | Config,
127 public TimelineScheduler()
128 => this.timer = new AsyncTimer(this.TimerCallback);
130 public void RefreshSchedule()
132 if (this.preventTimerUpdate)
133 return; // TimerCallback 内で更新されるのでここは単に無視してよい
136 this.timer.Change(this.NextTimerDelay(), Timeout.InfiniteTimeSpan);
138 this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
141 public void SystemResumed()
143 if (!this.EnableUpdateSystemResume)
146 this.SystemResumedAt = DateTimeUtc.Now;
147 this.systemResumeMode = true;
148 this.RefreshSchedule();
151 private async Task TimerCallback()
155 this.preventTimerUpdate = true;
157 if (this.systemResumeMode)
158 await this.TimerCallback_AfterSystemResume().ConfigureAwait(false);
160 await this.TimerCallback_Normal().ConfigureAwait(false);
164 this.preventTimerUpdate = false;
165 this.RefreshSchedule();
169 private async Task TimerCallback_Normal()
171 var now = DateTimeUtc.Now;
172 var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する
173 var tasks = UpdateTask.None;
175 if (this.EnableUpdateHome)
177 var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
178 if (nextScheduledHome - now < round)
179 tasks |= UpdateTask.Home;
182 if (this.EnableUpdateMention)
184 var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
185 if (nextScheduledMention - now < round)
186 tasks |= UpdateTask.Mention;
189 if (this.EnableUpdateDm)
191 var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
192 if (nextScheduledDm - now < round)
193 tasks |= UpdateTask.Dm;
196 if (this.EnableUpdatePublicSearch)
198 var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
199 if (nextScheduledPublicSearch - now < round)
200 tasks |= UpdateTask.PublicSearch;
203 if (this.EnableUpdateUser)
205 var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
206 if (nextScheduledUser - now < round)
207 tasks |= UpdateTask.User;
210 if (this.EnableUpdateList)
212 var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
213 if (nextScheduledList - now < round)
214 tasks |= UpdateTask.List;
217 if (this.EnableUpdateConfig)
219 var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
220 if (nextScheduledConfig - now < round)
221 tasks |= UpdateTask.Config;
224 await this.RunUpdateTasks(tasks, now).ConfigureAwait(false);
227 private async Task TimerCallback_AfterSystemResume()
229 // systemResumeMode では一定期間経過後に全てのタイムラインを更新する
230 var now = DateTimeUtc.Now;
232 var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
233 if (nextScheduledUpdateAll - now < TimeSpan.Zero)
235 this.systemResumeMode = false;
236 await this.RunUpdateTasks(UpdateTask.All, now).ConfigureAwait(false);
240 private async Task RunUpdateTasks(UpdateTask tasks, DateTimeUtc now)
242 var updateTasks = new List<Task>(capacity: 7);
244 // LastUpdate* を次の時刻に更新してから Update* を実行すること
245 // (LastUpdate* が更新されずに Update* が例外を投げると無限ループに陥る)
247 if ((tasks & UpdateTask.Home) == UpdateTask.Home)
249 this.LastUpdateHome = now;
250 if (this.UpdateHome != null)
251 updateTasks.Add(this.UpdateHome());
254 if ((tasks & UpdateTask.Mention) == UpdateTask.Mention)
256 this.LastUpdateMention = now;
257 if (this.UpdateMention != null)
258 updateTasks.Add(this.UpdateMention());
261 if ((tasks & UpdateTask.Dm) == UpdateTask.Dm)
263 this.LastUpdateDm = now;
264 if (this.UpdateDm != null)
265 updateTasks.Add(this.UpdateDm());
268 if ((tasks & UpdateTask.PublicSearch) == UpdateTask.PublicSearch)
270 this.LastUpdatePublicSearch = now;
271 if (this.UpdatePublicSearch != null)
272 updateTasks.Add(this.UpdatePublicSearch());
275 if ((tasks & UpdateTask.User) == UpdateTask.User)
277 this.LastUpdateUser = now;
278 if (this.UpdateUser != null)
279 updateTasks.Add(this.UpdateUser());
282 if ((tasks & UpdateTask.List) == UpdateTask.List)
284 this.LastUpdateList = now;
285 if (this.UpdateList != null)
286 updateTasks.Add(this.UpdateList());
289 if ((tasks & UpdateTask.Config) == UpdateTask.Config)
291 this.LastUpdateConfig = now;
292 if (this.UpdateConfig != null)
293 updateTasks.Add(this.UpdateConfig());
296 await Task.WhenAll(updateTasks).ConfigureAwait(false);
299 private TimeSpan NextTimerDelay()
303 if (this.systemResumeMode)
305 // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
306 var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
307 delay = nextScheduledUpdateAll - DateTimeUtc.Now;
309 return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
313 var min = DateTimeUtc.MaxValue;
315 if (this.EnableUpdateHome)
317 var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
318 if (nextScheduledHome < min)
319 min = nextScheduledHome;
322 if (this.EnableUpdateMention)
324 var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
325 if (nextScheduledMention < min)
326 min = nextScheduledMention;
329 if (this.EnableUpdateDm)
331 var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
332 if (nextScheduledDm < min)
333 min = nextScheduledDm;
336 if (this.EnableUpdatePublicSearch)
338 var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
339 if (nextScheduledPublicSearch < min)
340 min = nextScheduledPublicSearch;
343 if (this.EnableUpdateUser)
345 var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
346 if (nextScheduledUser < min)
347 min = nextScheduledUser;
350 if (this.EnableUpdateList)
352 var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
353 if (nextScheduledList < min)
354 min = nextScheduledList;
357 if (this.EnableUpdateConfig)
359 var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
360 if (nextScheduledConfig < min)
361 min = nextScheduledConfig;
364 delay = min - DateTimeUtc.Now;
366 return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;