File: Rendering\WebAssemblyDispatcher.cs
Web Access
Project: src\src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj (Microsoft.AspNetCore.Components.WebAssembly)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering;
 
// When Blazor is deployed with multi-threaded runtime, WebAssemblyDispatcher will help to dispatch all Blazor JS interop calls to the main thread.
// This is necessary because all JS objects have thread affinity. They are only available on the thread (WebWorker) which created them.
// Also DOM is only available on the main (browser) thread.
// Because all of the Dispatcher.InvokeAsync methods return Task, we don't need to propagate errors via OnUnhandledException handler
internal sealed class WebAssemblyDispatcher : Dispatcher
{
    internal static SynchronizationContext? _mainSynchronizationContext;
    internal static int _mainManagedThreadId;
 
    // we really need the UI thread not just the right context, because JS objects have thread affinity
    public override bool CheckAccess() => _mainManagedThreadId == Environment.CurrentManagedThreadId;
 
    public override Task InvokeAsync(Action workItem)
    {
        ArgumentNullException.ThrowIfNull(workItem);
        if (CheckAccess())
        {
            // this branch executes on correct thread and solved JavaScript objects thread affinity
            // but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
            workItem();
            // it can throw synchronously, same as RendererSynchronizationContextDispatcher
            return Task.CompletedTask;
        }
 
        var tcs = new TaskCompletionSource();
 
        // RendererSynchronizationContext doesn't need to deal with thread affinity and so it could execute jobs on calling thread as optimization.
        // we could not do it for WASM/JavaScript, because we need to solve for thread affinity of JavaScript objects, so we always Post into the queue.
        _mainSynchronizationContext!.Post(static (object? o) =>
        {
            var state = ((TaskCompletionSource tcs, Action workItem))o!;
            try
            {
                state.workItem();
                state.tcs.SetResult();
            }
            catch (Exception ex)
            {
                state.tcs.SetException(ex);
            }
        }, (tcs, workItem));
 
        return tcs.Task;
    }
 
    public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
    {
        ArgumentNullException.ThrowIfNull(workItem);
        if (CheckAccess())
        {
            // it can throw synchronously, same as RendererSynchronizationContextDispatcher
            return Task.FromResult(workItem());
        }
 
        var tcs = new TaskCompletionSource<TResult>();
 
        _mainSynchronizationContext!.Post(static (object? o) =>
        {
            var state = ((TaskCompletionSource<TResult> tcs, Func<TResult> workItem))o!;
            try
            {
                var res = state.workItem();
                state.tcs.SetResult(res);
            }
            catch (Exception ex)
            {
                state.tcs.SetException(ex);
            }
        }, (tcs, workItem));
 
        return tcs.Task;
    }
 
    public override Task InvokeAsync(Func<Task> workItem)
    {
        ArgumentNullException.ThrowIfNull(workItem);
        if (CheckAccess())
        {
            // this branch executes on correct thread and solved JavaScript objects thread affinity
            // but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
            return workItem();
            // it can throw synchronously, same as RendererSynchronizationContextDispatcher
        }
 
        var tcs = new TaskCompletionSource();
 
        _mainSynchronizationContext!.Post(static (object? o) =>
        {
            var state = ((TaskCompletionSource tcs, Func<Task> workItem))o!;
 
            try
            {
                state.workItem().ContinueWith(t =>
                {
                    if (t.IsFaulted)
                    {
                        state.tcs.SetException(t.Exception);
                    }
                    else if (t.IsCanceled)
                    {
                        state.tcs.SetCanceled();
                    }
                    else
                    {
                        state.tcs.SetResult();
                    }
                }, TaskScheduler.FromCurrentSynchronizationContext());
            }
            catch (Exception ex)
            {
                // it could happen that the workItem will throw synchronously
                state.tcs.SetException(ex);
            }
        }, (tcs, workItem));
 
        return tcs.Task;
    }
 
    public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
    {
        ArgumentNullException.ThrowIfNull(workItem);
        if (CheckAccess())
        {
            // this branch executes on correct thread and solved JavaScript objects thread affinity
            // but it executes out of order, if there are some pending jobs in the _mainSyncContext already, same as RendererSynchronizationContextDispatcher
            return workItem();
            // it can throw synchronously, same as RendererSynchronizationContextDispatcher
        }
 
        var tcs = new TaskCompletionSource<TResult>();
 
        _mainSynchronizationContext!.Post(static (object? o) =>
        {
            var state = ((TaskCompletionSource<TResult> tcs, Func<Task<TResult>> workItem))o!;
            try
            {
                state.workItem().ContinueWith(t =>
                {
                    if (t.IsFaulted)
                    {
                        state.tcs.SetException(t.Exception);
                    }
                    else if (t.IsCanceled)
                    {
                        state.tcs.SetCanceled();
                    }
                    else
                    {
                        state.tcs.SetResult(t.Result);
                    }
                }, TaskScheduler.FromCurrentSynchronizationContext());
            }
            catch (Exception ex)
            {
                state.tcs.SetException(ex);
            }
        }, (tcs, workItem));
 
        return tcs.Task;
    }
}