Forge
csharpdeeb25a2
1using System.Threading.Channels;
2
3namespace CascadeIDE.Features.Editor.Application;
4
5/// <summary>
6/// Hi-freq → bounded channel (capacity 1, drop-oldest) → минимальный интервал между
7/// срабатываниями → UI thread. ADR 0103, не <see cref="IDataBus"/>.
8/// </summary>
9public sealed class EditorStabilizedInputThrottler : IAsyncDisposable
10{
11 private readonly IUiScheduler _ui;
12 private readonly TimeSpan _minInterval;
13 private readonly Channel<EditorInputDelta> _channel;
14 private Task? _loop;
15 private CancellationTokenSource? _runCts;
16
17 public EditorStabilizedInputThrottler(IUiScheduler ui, TimeSpan minInterval)
18 {
19 _ui = ui;
20 _minInterval = minInterval;
21 _channel = Channel.CreateBounded<EditorInputDelta>(new BoundedChannelOptions(1)
22 {
23 FullMode = BoundedChannelFullMode.DropOldest,
24 SingleReader = true,
25 SingleWriter = true,
26 });
27 }
28
29 public bool TryPost(EditorInputDelta delta) =>
30 _channel.Writer.TryWrite(delta);
31
32 public void Start(Action<EditorInputDelta> onStabilized, CancellationToken appCancellation)
33 {
34 if (_loop is not null)
35 return;
36
37 _runCts = CancellationTokenSource.CreateLinkedTokenSource(appCancellation);
38 var token = _runCts.Token;
39 var reader = _channel.Reader;
40
41 _loop = Task.Run(
42 async () =>
43 {
44 var lastOut = DateTime.MinValue;
45 while (!token.IsCancellationRequested)
46 {
47 EditorInputDelta d;
48 try
49 {
50 d = await reader.ReadAsync(token).ConfigureAwait(false);
51 }
52 catch (OperationCanceledException)
53 {
54 break;
55 }
56 catch (ChannelClosedException)
57 {
58 break;
59 }
60
61 var now = DateTime.UtcNow;
62 var since = now - lastOut;
63 if (since < _minInterval && _minInterval > TimeSpan.Zero)
64 {
65 var wait = _minInterval - since;
66 try
67 {
68 await Task.Delay(wait, token).ConfigureAwait(false);
69 }
70 catch (OperationCanceledException)
71 {
72 break;
73 }
74 }
75
76 var copy = d;
77 _ui.Post(() =>
78 {
79 if (!token.IsCancellationRequested)
80 onStabilized(copy);
81 });
82 lastOut = DateTime.UtcNow;
83 }
84 },
85 CancellationToken.None);
86 }
87
88 public async ValueTask DisposeAsync()
89 {
90 _runCts?.Cancel();
91 _channel.Writer.TryComplete();
92 if (_loop is not null)
93 {
94 try
95 {
96 await _loop.ConfigureAwait(false);
97 }
98 catch
99 {
100 // loop cancelled
101 }
102 }
103 _runCts?.Dispose();
104 _runCts = null;
105 _loop = null;
106 }
107}
108
View only · write via MCP/CIDE