// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using MessagePack; using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.Server.Circuits; using Microsoft.AspNetCore.SignalR; using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Blazor.Browser.Rendering { internal class RemoteRenderer : Renderer { // The purpose of the timeout is just to ensure server resources are released at some // point if the client disconnects without sending back an ACK after a render private const int TimeoutMilliseconds = 60 * 1000; private readonly int _id; private readonly IClientProxy _client; private readonly IJSRuntime _jsRuntime; private readonly RendererRegistry _rendererRegistry; private readonly ConcurrentDictionary> _pendingRenders = new ConcurrentDictionary>(); private long _nextRenderId = 1; /// /// Notifies when a rendering exception occured. /// public event EventHandler UnhandledException; /// /// Creates a new . /// /// The . /// The . /// The . /// The . public RemoteRenderer( IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client) : base(serviceProvider) { _rendererRegistry = rendererRegistry; _jsRuntime = jsRuntime; _client = client; _id = _rendererRegistry.Add(this); } /// /// Attaches a new root component to the renderer, /// causing it to be displayed in the specified DOM element. /// /// The type of the component. /// A CSS selector that uniquely identifies a DOM element. public void AddComponent(string domElementSelector) where TComponent: IComponent { AddComponent(typeof(TComponent), domElementSelector); } /// /// Associates the with the , /// causing it to be displayed in the specified DOM element. /// /// The type of the component. /// A CSS selector that uniquely identifies a DOM element. public void AddComponent(Type componentType, string domElementSelector) { var component = InstantiateComponent(componentType); var componentId = AssignRootComponentId(component); var attachComponentTask = _jsRuntime.InvokeAsync( "Blazor._internal.attachRootComponentToElement", _id, domElementSelector, componentId); CaptureAsyncExceptions(attachComponentTask); RenderRootComponent(componentId); } /// /// Disposes the instance. /// public void Dispose() { _rendererRegistry.TryRemove(_id); } /// protected override Task UpdateDisplayAsync(in RenderBatch batch) { // Note that we have to capture the data as a byte[] synchronously here, because // SignalR's SendAsync can wait an arbitrary duration before serializing the params. // The RenderBatch buffer will get reused by subsequent renders, so we need to // snapshot its contents now. // TODO: Consider using some kind of array pool instead of allocating a new // buffer on every render. var batchBytes = MessagePackSerializer.Serialize(batch, RenderBatchFormatterResolver.Instance); // Prepare to track the render process with a timeout var renderId = Interlocked.Increment(ref _nextRenderId); var pendingRenderInfo = new AutoCancelTaskCompletionSource(TimeoutMilliseconds); _pendingRenders[renderId] = pendingRenderInfo; // Send the render batch to the client // If the "send" operation fails (synchronously or asynchronously), abort // the whole render with that exception try { _client.SendAsync("JS.RenderBatch", _id, renderId, batchBytes).ContinueWith(sendTask => { if (sendTask.IsFaulted) { pendingRenderInfo.TrySetException(sendTask.Exception); } }); } catch (Exception syncException) { pendingRenderInfo.TrySetException(syncException); } // When the render is completed (success, fail, or timeout), stop tracking it return pendingRenderInfo.Task.ContinueWith(task => { _pendingRenders.TryRemove(renderId, out var ignored); if (task.IsFaulted) { UnhandledException?.Invoke(this, task.Exception); } }); } public void OnRenderCompleted(long renderId, string errorMessageOrNull) { if (_pendingRenders.TryGetValue(renderId, out var pendingRenderInfo)) { if (errorMessageOrNull == null) { pendingRenderInfo.TrySetResult(null); } else { pendingRenderInfo.TrySetException( new RemoteRendererException(errorMessageOrNull)); } } } private void CaptureAsyncExceptions(Task task) { task.ContinueWith(t => { if (t.IsFaulted) { UnhandledException?.Invoke(this, t.Exception); } }); } } }