Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions src/Butil/Bit.Butil/BitButil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,55 @@ public static class BitButil
{
public static IServiceCollection AddBitButilServices(this IServiceCollection services)
{
services.AddTransient<Clipboard>();
services.AddTransient<Console>();
services.AddTransient<Cookie>();
services.AddTransient<Crypto>();
services.AddTransient<Document>();
services.AddTransient<History>();
services.AddTransient<Keyboard>();
services.AddTransient<LocalStorage>();
services.AddTransient<SessionStorage>();
services.AddTransient<Location>();
services.AddTransient<Navigator>();
services.AddTransient<Notification>();
services.AddTransient<Screen>();
services.AddTransient<ScreenOrientation>();
services.AddTransient<UserAgent>();
services.AddTransient<VisualViewport>();
services.AddTransient<Window>();
services.AddTransient<WebAuthn>();
// Scoped matches Blazor's "one circuit / one WASM app instance per user" model.
// Transient would create a fresh wrapper on every @inject, fragmenting per-instance
// listener bookkeeping and keeping captured component delegates alive longer than
// the component itself.
services.AddScoped<Clipboard>();
services.AddScoped<Console>();
services.AddScoped<Cookie>();
services.AddScoped<CookieStore>();
services.AddScoped<Crypto>();
services.AddScoped<Battery>();
services.AddScoped<BackgroundSync>();
services.AddScoped<BroadcastChannel>();
services.AddScoped<CacheStorage>();
services.AddScoped<ContactPicker>();
services.AddScoped<Document>();
services.AddScoped<EyeDropper>();
services.AddScoped<Fetch>();
services.AddScoped<FileReader>();
services.AddScoped<Geolocation>();
services.AddScoped<History>();
services.AddScoped<IdleDetector>();
services.AddScoped<IndexedDb>();
services.AddScoped<Keyboard>();
services.AddScoped<LocalStorage>();
services.AddScoped<SessionStorage>();
services.AddScoped<Location>();
services.AddScoped<MediaDevices>();
services.AddScoped<Navigator>();
services.AddScoped<NetworkInformation>();
services.AddScoped<Nfc>();
services.AddScoped<Notification>();
services.AddScoped<ObjectUrls>();
services.AddScoped<Performance>();
services.AddScoped<Permissions>();
services.AddScoped<Push>();
services.AddScoped<Reporting>();
services.AddScoped<Screen>();
services.AddScoped<ScreenOrientation>();
services.AddScoped<ServiceWorker>();
services.AddScoped<SpeechRecognition>();
services.AddScoped<SpeechSynthesis>();
services.AddScoped<StorageManager>();
services.AddScoped<UserAgent>();
services.AddScoped<VisualViewport>();
services.AddScoped<WakeLock>();
services.AddScoped<WebAudio>();
services.AddScoped<WebLocks>();
services.AddScoped<Window>();
services.AddScoped<WebAuthn>();
Comment thread
msynk marked this conversation as resolved.

return services;
}
Expand Down
30 changes: 17 additions & 13 deletions src/Butil/Bit.Butil/Extensions/InternalJSRuntimeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Threading;
using System.Reflection;
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;
Expand Down Expand Up @@ -57,19 +56,24 @@ internal static ValueTask InvokeVoid(this IJSRuntime jsRuntime, string identifie
}


[SuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "<Pending>")]
internal static bool IsJsRuntimeInvalid(this IJSRuntime jsRuntime)
/// <summary>
/// Returns true when calling into JavaScript right now would either be impossible
/// (no runtime / pre-render) or guaranteed to fail (disposed circuit).
/// </summary>
/// <remarks>
/// We deliberately avoid reflecting over private fields of <c>RemoteJSRuntime</c>
/// or <c>WebViewJSRuntime</c>; those internals have changed across .NET releases.
/// Instead we rely on the only documented sentinel — the
/// <c>UnsupportedJavaScriptRuntime</c> type used during static SSR / pre-render —
/// and let actual disconnect surface as <see cref="JSDisconnectedException"/> at
/// the call site, which callers already catch.
/// </remarks>
internal static bool IsJsRuntimeInvalid(this IJSRuntime? jsRuntime)
{
if (jsRuntime is null) return false;
if (jsRuntime is null) return true;

var type = jsRuntime.GetType();

return type.Name switch
{
"UnsupportedJavaScriptRuntime" => true, // Prerendering
"RemoteJSRuntime" => (bool)type.GetProperty("IsInitialized")!.GetValue(jsRuntime)! is false, // Blazor server
"WebViewJSRuntime" => type.GetField("_ipcSender", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(jsRuntime) is null, // Blazor Hybrid
_ => false // Blazor WASM
};
// During pre-rendering ASP.NET injects an UnsupportedJavaScriptRuntime that
// throws on every call. We special-case it to keep prerender silent.
return jsRuntime.GetType().Name == "UnsupportedJavaScriptRuntime";
}
}
40 changes: 12 additions & 28 deletions src/Butil/Bit.Butil/Extensions/JSRuntimeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop;
Expand Down Expand Up @@ -60,20 +59,14 @@ public static class JSRuntimeExtensions
{
if (jsRuntime is IJSInProcessRuntime jsInProcessRuntime)
{
try
{
return ValueTask.FromResult(jsInProcessRuntime.Invoke<TResult>(identifier, args));
}
catch (JsonException ex)
{
System.Console.Error.WriteLine($"Error invoking '{identifier}' using {nameof(IJSInProcessRuntime)}. A JSON-related issue occurred: {ex.Message}.");
return ValueTask.FromResult(default(TResult)!);
}
}
else
{
return jsRuntime.InvokeAsync<TResult>(identifier, cancellationToken, args);
// We deliberately do not catch JsonException here. Calling the synchronous
// Invoke<T> against a JS function that returns a Promise produces a JSON
// payload that cannot deserialize to TResult; surfacing the error makes the
// mistake visible instead of silently returning default(TResult).
return ValueTask.FromResult(jsInProcessRuntime.Invoke<TResult>(identifier, args));
}

return jsRuntime.InvokeAsync<TResult>(identifier, cancellationToken, args);
}


Expand Down Expand Up @@ -117,21 +110,12 @@ public static ValueTask FastInvokeVoidAsync(this IJSRuntime jsRuntime, string id
{
if (jsRuntime is IJSInProcessRuntime jsInProcessRuntime)
{
try
{
jsInProcessRuntime.Invoke<IJSVoidResult>(identifier, args);
return ValueTask.CompletedTask;
}
catch (JsonException ex)
{
System.Console.Error.WriteLine($"Error invoking '{identifier}' using {nameof(IJSInProcessRuntime)}. A JSON-related issue occurred: {ex.Message}.");
return ValueTask.CompletedTask;
}
}
else
{
return jsRuntime.InvokeVoidAsync(identifier, cancellationToken, args);
// Don't swallow JsonException — see FastInvokeAsync<TResult> for rationale.
jsInProcessRuntime.Invoke<IJSVoidResult>(identifier, args);
return ValueTask.CompletedTask;
}

return jsRuntime.InvokeVoidAsync(identifier, cancellationToken, args);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Concurrent;
using Microsoft.JSInterop;

namespace Bit.Butil;

public static class BroadcastChannelListenersManager
{
internal const string MessageMethodName = "InvokeBroadcastChannelMessage";
internal const string ErrorMethodName = "InvokeBroadcastChannelError";

private static readonly ConcurrentDictionary<Guid, Listener> Listeners = [];

internal static Guid AddListener(Action<System.Text.Json.JsonElement>? onMessage, Action? onError)
{
var id = Guid.NewGuid();
Listeners.TryAdd(id, new Listener { OnMessage = onMessage, OnError = onError });
return id;
}

internal static void RemoveListener(Guid id) => Listeners.TryRemove(id, out _);

[JSInvokable(MessageMethodName)]
public static void InvokeMessage(Guid id, System.Text.Json.JsonElement data)
{
if (Listeners.TryGetValue(id, out var listener)) listener.OnMessage?.Invoke(data);
}

[JSInvokable(ErrorMethodName)]
public static void InvokeError(Guid id)
{
if (Listeners.TryGetValue(id, out var listener)) listener.OnError?.Invoke();
}

private class Listener
{
public Action<System.Text.Json.JsonElement>? OnMessage { get; set; }
public Action? OnError { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using Microsoft.JSInterop;

namespace Bit.Butil;

public static class DomClipboardEventListenersManager
{
internal const string InvokeMethodName = "InvokeClipboardEvent";

private static readonly ConcurrentDictionary<Guid, Listener> Listeners = [];

internal static Guid SetListener(Action<ButilClipboardEventArgs> action, string element, object options)
{
var id = Guid.NewGuid();
Listeners.TryAdd(id, new Listener { Action = action, Element = element, Options = options });
return id;
}

internal static Guid[] RemoveListener(Action<ButilClipboardEventArgs> action, string element, object options)
{
var toRemove = Listeners
.Where(l => l.Value.Action == action && l.Value.Element == element && l.Value.Options == options)
.ToArray();

return toRemove.Select(l => { Listeners.TryRemove(l.Key, out _); return l.Key; }).ToArray();
}
internal static void RemoveById(Guid id) => Listeners.TryRemove(id, out _);


[JSInvokable(InvokeMethodName)]
public static void Invoke(Guid id, ButilClipboardEventArgs args)
{
if (Listeners.TryGetValue(id, out var listener)) listener.Action.Invoke(args);
}

private class Listener
{
public string Element { get; set; } = string.Empty;
public object Options { get; set; } = default!;
public Action<ButilClipboardEventArgs> Action { get; set; } = default!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using Microsoft.JSInterop;

namespace Bit.Butil;

public static class DomCompositionEventListenersManager
{
internal const string InvokeMethodName = "InvokeCompositionEvent";

private static readonly ConcurrentDictionary<Guid, Listener> Listeners = [];

internal static Guid SetListener(Action<ButilCompositionEventArgs> action, string element, object options)
{
var id = Guid.NewGuid();
Listeners.TryAdd(id, new Listener { Action = action, Element = element, Options = options });
return id;
}

internal static Guid[] RemoveListener(Action<ButilCompositionEventArgs> action, string element, object options)
{
var toRemove = Listeners
.Where(l => l.Value.Action == action && l.Value.Element == element && l.Value.Options == options)
.ToArray();

return toRemove.Select(l => { Listeners.TryRemove(l.Key, out _); return l.Key; }).ToArray();
}
internal static void RemoveById(Guid id) => Listeners.TryRemove(id, out _);


[JSInvokable(InvokeMethodName)]
public static void Invoke(Guid id, ButilCompositionEventArgs args)
{
if (Listeners.TryGetValue(id, out var listener)) listener.Action.Invoke(args);
}

private class Listener
{
public string Element { get; set; } = string.Empty;
public object Options { get; set; } = default!;
public Action<ButilCompositionEventArgs> Action { get; set; } = default!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using Microsoft.JSInterop;

namespace Bit.Butil;

public static class DomDragEventListenersManager
{
internal const string InvokeMethodName = "InvokeDragEvent";

private static readonly ConcurrentDictionary<Guid, Listener> Listeners = [];

internal static Guid SetListener(Action<ButilDragEventArgs> action, string element, object options)
{
var id = Guid.NewGuid();
Listeners.TryAdd(id, new Listener { Action = action, Element = element, Options = options });
return id;
}

internal static Guid[] RemoveListener(Action<ButilDragEventArgs> action, string element, object options)
{
var toRemove = Listeners
.Where(l => l.Value.Action == action && l.Value.Element == element && l.Value.Options == options)
.ToArray();

return toRemove.Select(l => { Listeners.TryRemove(l.Key, out _); return l.Key; }).ToArray();
}
internal static void RemoveById(Guid id) => Listeners.TryRemove(id, out _);


[JSInvokable(InvokeMethodName)]
public static void Invoke(Guid id, ButilDragEventArgs args)
{
if (Listeners.TryGetValue(id, out var listener)) listener.Action.Invoke(args);
}

private class Listener
{
public string Element { get; set; } = string.Empty;
public object Options { get; set; } = default!;
public Action<ButilDragEventArgs> Action { get; set; } = default!;
}
}
Loading
Loading