diff --git a/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj b/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj
index 02d5e16..9a64e07 100644
--- a/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj
+++ b/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj
@@ -15,6 +15,10 @@
+
+
+
+
diff --git a/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts b/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts
index fbe9875..889f1fe 100644
--- a/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts
+++ b/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts
@@ -1,17 +1,17 @@
import '@dotnet/jsinterop/dist/Microsoft.JSInterop';
import '@browserjs/GlobalExports';
-import { OutOfProcessRenderBatch } from '@browserjs/Rendering/RenderBatch/OutOfProcessRenderBatch';
import { setEventDispatcher } from '@browserjs/Rendering/RendererEventDispatcher';
import { internalFunctions as navigationManagerFunctions } from '@browserjs/Services/NavigationManager';
-import { renderBatch } from '@browserjs/Rendering/Renderer';
import { decode } from 'base64-arraybuffer';
import * as ipc from './IPC';
+import { RenderQueue } from './RenderQueue';
function boot() {
setEventDispatcher((eventDescriptor, eventArgs) => DotNet.invokeMethodAsync('WebWindow.Blazor', 'DispatchEvent', eventDescriptor, JSON.stringify(eventArgs)));
navigationManagerFunctions.listenForNavigationEvents((uri: string, intercepted: boolean) => {
return DotNet.invokeMethodAsync('WebWindow.Blazor', 'NotifyLocationChanged', uri, intercepted);
});
+ const renderQueue = RenderQueue.getOrCreate();
// Configure the mechanism for JS<->NET calls
DotNet.attachDispatcher({
@@ -33,9 +33,9 @@ function boot() {
DotNet.jsCallDispatcher.endInvokeDotNetFromJS(callId, success, resultOrError);
});
- ipc.on('JS.RenderBatch', (rendererId, batchBase64) => {
- var batchData = new Uint8Array(decode(batchBase64));
- renderBatch(rendererId, new OutOfProcessRenderBatch(batchData));
+ ipc.on('JS.RenderBatch', (batchId, batchBase64) => {
+ const batchData = new Uint8Array(decode(batchBase64));
+ renderQueue.processBatch(batchId, batchData);
});
ipc.on('JS.Error', (message) => {
diff --git a/src/WebWindow.Blazor.JS/src/RenderQueue.ts b/src/WebWindow.Blazor.JS/src/RenderQueue.ts
new file mode 100644
index 0000000..07e6b0d
--- /dev/null
+++ b/src/WebWindow.Blazor.JS/src/RenderQueue.ts
@@ -0,0 +1,66 @@
+import '@dotnet/jsinterop/dist/Microsoft.JSInterop';
+import { renderBatch } from '@browserjs/Rendering/Renderer';
+import { OutOfProcessRenderBatch } from '@browserjs/Rendering/RenderBatch/OutOfProcessRenderBatch';
+
+export class RenderQueue {
+ private static instance: RenderQueue;
+
+ private nextBatchId = 2;
+
+ private fatalError?: string;
+
+ public browserRendererId: number;
+
+ public constructor(browserRendererId: number) {
+ this.browserRendererId = browserRendererId;
+ }
+
+ public static getOrCreate(): RenderQueue {
+ if (!RenderQueue.instance) {
+ RenderQueue.instance = new RenderQueue(0);
+ }
+
+ return this.instance;
+ }
+
+ public async processBatch(receivedBatchId: number, batchData: Uint8Array): Promise {
+ if (receivedBatchId < this.nextBatchId) {
+ await this.completeBatch(receivedBatchId);
+ return;
+ }
+
+ if (receivedBatchId > this.nextBatchId) {
+ if (this.fatalError) {
+ console.log(`Received a new batch ${receivedBatchId} but errored out on a previous batch ${this.nextBatchId - 1}`);
+ await DotNet.invokeMethodAsync('WebWindow.Blazor', 'OnRenderCompleted', this.nextBatchId - 1, this.fatalError.toString());
+ return;
+ }
+ return;
+ }
+
+ try {
+ this.nextBatchId++;
+ renderBatch(this.browserRendererId, new OutOfProcessRenderBatch(batchData));
+ await this.completeBatch(receivedBatchId);
+ } catch (error) {
+ this.fatalError = error.toString();
+ console.error(`There was an error applying batch ${receivedBatchId}.`);
+
+ // If there's a rendering exception, notify server *and* throw on client
+ DotNet.invokeMethodAsync('WebWindow.Blazor', 'OnRenderCompleted', receivedBatchId, error.toString());
+ throw error;
+ }
+ }
+
+ public getLastBatchid(): number {
+ return this.nextBatchId - 1;
+ }
+
+ private async completeBatch(batchId: number): Promise {
+ try {
+ await DotNet.invokeMethodAsync('WebWindow.Blazor', 'OnRenderCompleted', batchId, null);
+ } catch {
+ console.warn(`Failed to deliver completion notification for render '${batchId}'.`);
+ }
+ }
+}
diff --git a/src/WebWindow.Blazor/DesktopRenderer.cs b/src/WebWindow.Blazor/DesktopRenderer.cs
index de6ddf8..7755c76 100644
--- a/src/WebWindow.Blazor/DesktopRenderer.cs
+++ b/src/WebWindow.Blazor/DesktopRenderer.cs
@@ -5,8 +5,10 @@
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using System;
+using System.Collections.Concurrent;
using System.IO;
using System.Reflection;
+using System.Threading;
using System.Threading.Tasks;
namespace WebWindows.Blazor
@@ -17,11 +19,16 @@ namespace WebWindows.Blazor
internal class DesktopRenderer : Renderer
{
+ private static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true));
+
private const int RendererId = 0; // Not relevant, since we have only one renderer in Desktop
private readonly IPC _ipc;
private readonly IJSRuntime _jsRuntime;
private static readonly Type _writer;
private static readonly MethodInfo _writeMethod;
+ internal readonly ConcurrentQueue _unacknowledgedRenderBatches = new ConcurrentQueue();
+ private bool _disposing = false;
+ private long _nextRenderId = 1;
public override Dispatcher Dispatcher { get; } = NullDispatcher.Instance;
@@ -78,6 +85,12 @@ public Task AddComponentAsync(Type componentType, string domElementSelector)
///
protected override Task UpdateDisplayAsync(in RenderBatch batch)
{
+ if (_disposing)
+ {
+ // We are being disposed, so do no work.
+ return CanceledTask;
+ }
+
string base64;
using (var memoryStream = new MemoryStream())
{
@@ -91,13 +104,111 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch)
base64 = Convert.ToBase64String(batchBytes);
}
- _ipc.Send("JS.RenderBatch", RendererId, base64);
+ var renderId = Interlocked.Increment(ref _nextRenderId);
+
+ var pendingRender = new UnacknowledgedRenderBatch(
+ renderId,
+ new TaskCompletionSource