55using Microsoft . Extensions . Logging ;
66using Microsoft . JSInterop ;
77using System ;
8+ using System . Collections . Concurrent ;
89using System . IO ;
910using System . Reflection ;
11+ using System . Threading ;
1012using System . Threading . Tasks ;
1113
1214namespace WebWindows . Blazor
@@ -17,11 +19,16 @@ namespace WebWindows.Blazor
1719
1820 internal class DesktopRenderer : Renderer
1921 {
22+ private static readonly Task CanceledTask = Task . FromCanceled ( new CancellationToken ( canceled : true ) ) ;
23+
2024 private const int RendererId = 0 ; // Not relevant, since we have only one renderer in Desktop
2125 private readonly IPC _ipc ;
2226 private readonly IJSRuntime _jsRuntime ;
2327 private static readonly Type _writer ;
2428 private static readonly MethodInfo _writeMethod ;
29+ internal readonly ConcurrentQueue < UnacknowledgedRenderBatch > _unacknowledgedRenderBatches = new ConcurrentQueue < UnacknowledgedRenderBatch > ( ) ;
30+ private bool _disposing = false ;
31+ private long _nextRenderId = 1 ;
2532
2633 public override Dispatcher Dispatcher { get ; } = NullDispatcher . Instance ;
2734
@@ -78,6 +85,12 @@ public Task AddComponentAsync(Type componentType, string domElementSelector)
7885 /// <inheritdoc />
7986 protected override Task UpdateDisplayAsync ( in RenderBatch batch )
8087 {
88+ if ( _disposing )
89+ {
90+ // We are being disposed, so do no work.
91+ return CanceledTask ;
92+ }
93+
8194 string base64 ;
8295 using ( var memoryStream = new MemoryStream ( ) )
8396 {
@@ -91,13 +104,111 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch)
91104 base64 = Convert . ToBase64String ( batchBytes ) ;
92105 }
93106
94- _ipc . Send ( "JS.RenderBatch" , RendererId , base64 ) ;
107+ var renderId = Interlocked . Increment ( ref _nextRenderId ) ;
108+
109+ var pendingRender = new UnacknowledgedRenderBatch (
110+ renderId ,
111+ new TaskCompletionSource < object > ( ) ) ;
112+
113+ // Buffer the rendered batches no matter what. We'll send it down immediately when the client
114+ // is connected or right after the client reconnects.
115+
116+ _unacknowledgedRenderBatches . Enqueue ( pendingRender ) ;
95117
96- // TODO: Consider finding a way to get back a completion message from the Desktop side
97- // in case there was an error. We don't really need to wait for anything to happen, since
98- // this is not prerendering and we don't care how quickly the UI is updated, but it would
99- // be desirable to flow back errors.
100- return Task . CompletedTask ;
118+ _ipc . Send ( "JS.RenderBatch" , renderId , base64 ) ;
119+
120+ return pendingRender . CompletionSource . Task ;
121+ }
122+
123+ public Task OnRenderCompletedAsync ( long incomingBatchId , string errorMessageOrNull )
124+ {
125+ if ( _disposing )
126+ {
127+ // Disposing so don't do work.
128+ return Task . CompletedTask ;
129+ }
130+
131+ // When clients send acks we know for sure they received and applied the batch.
132+ // We send batches right away, and hold them in memory until we receive an ACK.
133+ // If one or more client ACKs get lost (e.g., with long polling, client->server delivery is not guaranteed)
134+ // we might receive an ack for a higher batch.
135+ // We confirm all previous batches at that point (because receiving an ack is guarantee
136+ // from the client that it has received and successfully applied all batches up to that point).
137+
138+ // If receive an ack for a previously acknowledged batch, its an error, as the messages are
139+ // guaranteed to be delivered in order, so a message for a render batch of 2 will never arrive
140+ // after a message for a render batch for 3.
141+ // If that were to be the case, it would just be enough to relax the checks here and simply skip
142+ // the message.
143+
144+ // A batch might get lost when we send it to the client, because the client might disconnect before receiving and processing it.
145+ // In this case, once it reconnects the server will re-send any unacknowledged batches, some of which the
146+ // client might have received and even believe it did send back an acknowledgement for. The client handles
147+ // those by re-acknowledging.
148+
149+ // Even though we're not on the renderer sync context here, it's safe to assume ordered execution of the following
150+ // line (i.e., matching the order in which we received batch completion messages) based on the fact that SignalR
151+ // synchronizes calls to hub methods. That is, it won't issue more than one call to this method from the same hub
152+ // at the same time on different threads.
153+
154+ if ( ! _unacknowledgedRenderBatches . TryPeek ( out var nextUnacknowledgedBatch ) || incomingBatchId < nextUnacknowledgedBatch . BatchId )
155+ {
156+ // TODO: Log duplicated batch ack.
157+ return Task . CompletedTask ;
158+ }
159+ else
160+ {
161+ var lastBatchId = nextUnacknowledgedBatch . BatchId ;
162+ // Order is important here so that we don't prematurely dequeue the last nextUnacknowledgedBatch
163+ while ( _unacknowledgedRenderBatches . TryPeek ( out nextUnacknowledgedBatch ) && nextUnacknowledgedBatch . BatchId <= incomingBatchId )
164+ {
165+ lastBatchId = nextUnacknowledgedBatch . BatchId ;
166+ // At this point the queue is definitely not full, we have at least emptied one slot, so we allow a further
167+ // full queue log entry the next time it fills up.
168+ _unacknowledgedRenderBatches . TryDequeue ( out _ ) ;
169+ ProcessPendingBatch ( errorMessageOrNull , nextUnacknowledgedBatch ) ;
170+ }
171+
172+ if ( lastBatchId < incomingBatchId )
173+ {
174+ // This exception is due to a bad client input, so we mark it as such to prevent logging it as a warning and
175+ // flooding the logs with warnings.
176+ throw new InvalidOperationException ( $ "Received an acknowledgement for batch with id '{ incomingBatchId } ' when the last batch produced was '{ lastBatchId } '.") ;
177+ }
178+
179+ // Normally we will not have pending renders, but it might happen that we reached the limit of
180+ // available buffered renders and new renders got queued.
181+ // Invoke ProcessBufferedRenderRequests so that we might produce any additional batch that is
182+ // missing.
183+
184+ // We return the task in here, but the caller doesn't await it.
185+ return Dispatcher . InvokeAsync ( ( ) =>
186+ {
187+ // Now we're on the sync context, check again whether we got disposed since this
188+ // work item was queued. If so there's nothing to do.
189+ if ( ! _disposing )
190+ {
191+ ProcessPendingRender ( ) ;
192+ }
193+ } ) ;
194+ }
195+ }
196+
197+ private void ProcessPendingBatch ( string errorMessageOrNull , UnacknowledgedRenderBatch entry )
198+ {
199+ CompleteRender ( entry . CompletionSource , errorMessageOrNull ) ;
200+ }
201+
202+ private void CompleteRender ( TaskCompletionSource < object > pendingRenderInfo , string errorMessageOrNull )
203+ {
204+ if ( errorMessageOrNull == null )
205+ {
206+ pendingRenderInfo . TrySetResult ( null ) ;
207+ }
208+ else
209+ {
210+ pendingRenderInfo . TrySetException ( new InvalidOperationException ( errorMessageOrNull ) ) ;
211+ }
101212 }
102213
103214 private async void CaptureAsyncExceptions ( ValueTask < object > task )
@@ -116,5 +227,29 @@ protected override void HandleException(Exception exception)
116227 {
117228 Console . WriteLine ( exception . ToString ( ) ) ;
118229 }
230+
231+ /// <inheritdoc />
232+ protected override void Dispose ( bool disposing )
233+ {
234+ _disposing = true ;
235+ while ( _unacknowledgedRenderBatches . TryDequeue ( out var entry ) )
236+ {
237+ entry . CompletionSource . TrySetCanceled ( ) ;
238+ }
239+ base . Dispose ( true ) ;
240+ }
241+
242+ internal readonly struct UnacknowledgedRenderBatch
243+ {
244+ public UnacknowledgedRenderBatch ( long batchId , TaskCompletionSource < object > completionSource )
245+ {
246+ BatchId = batchId ;
247+ CompletionSource = completionSource ;
248+ }
249+
250+ public long BatchId { get ; }
251+
252+ public TaskCompletionSource < object > CompletionSource { get ; }
253+ }
119254 }
120255}
0 commit comments