using Microsoft.JSInterop; using System; using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Blazor.Diagrams.Interop { public class BatchJsRuntime : IJSRuntime { private long _nextId = 1; private readonly ConcurrentQueue _calls; private readonly ConcurrentDictionary _cancellationRegistrations; private readonly IJSRuntime _jsRuntime; private Timer _timer; public BatchJsRuntime(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; _calls = new ConcurrentQueue(); _cancellationRegistrations = new ConcurrentDictionary(); } public JsonSerializerOptions JsonSerializerOptions { get; } = new JsonSerializerOptions { MaxDepth = 32, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; public ValueTask InvokeAsync(string identifier, object[] args) { return InvokeAsync(identifier, CancellationToken.None, args); } public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) { var id = Interlocked.Increment(ref _nextId); var tcs = new TaskCompletionSource(); var call = new JsCall(id, identifier, args, tcs, typeof(TValue)); _calls.Enqueue(call); // Handle cancellations if (cancellationToken.CanBeCanceled) { _cancellationRegistrations[id] = cancellationToken.Register(() => { tcs.TrySetCanceled(cancellationToken); if (_cancellationRegistrations.TryGetValue(id, out var registration)) { registration.Dispose(); } }); } if (_timer == null) { _timer = new Timer(OnTimerTick, null, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(50)); } return new ValueTask(tcs.Task); } private async void OnTimerTick(object state) { if (_calls.Count == 0) return; var currentCalls = new List(); var callJsObjects = new List(); while (_calls.TryDequeue(out var call)) { currentCalls.Add(call); callJsObjects.Add(new { identifier = call.Identifier, args = call.Args != null && call.Args.Length != 0 ? call.Args : null }); } var results = await _jsRuntime.InvokeAsync("batchJsInterop", callJsObjects); for (var i = 0; i < results.Length; i++) { var call = currentCalls[i]; var result = results[i]; if (result.Success) { object resultObj = result.ReturnValue == null ? null : ToObject((JsonElement)result.ReturnValue, call.Type, JsonSerializerOptions); TcsUtil.SetResult(call.Tcs, call.Type, resultObj); } else { TcsUtil.SetException(call.Tcs, call.Type, new BatchJsException(result.Error)); } } } private static object ToObject(JsonElement element, Type type, JsonSerializerOptions options = null) { var bufferWriter = new ArrayBufferWriter(); using (var writer = new Utf8JsonWriter(bufferWriter)) { element.WriteTo(writer); } return JsonSerializer.Deserialize(bufferWriter.WrittenSpan, type, options); } } }