using Iceshrimp.Frontend.Core.Schemas; using Iceshrimp.Shared.Schemas.SignalR; using Iceshrimp.Shared.Schemas.Web; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.AspNetCore.SignalR.Client; using TypedSignalR.Client; namespace Iceshrimp.Frontend.Core.Services; using NoteEvent = (StreamingTimeline timeline, NoteResponse note); internal class StreamingService( SessionService session, NavigationManager navigation, ILogger logger ) : IAsyncDisposable { private IStreamingHubServer? _hub; private HubConnection? _hubConnection; public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected; public async ValueTask DisposeAsync() { if (_hubConnection is not null) { await _hubConnection.DisposeAsync(); } } public event EventHandler? Notification; public event EventHandler? NotePublished; public event EventHandler? NoteUpdated; public event EventHandler? NoteDeleted; public event EventHandler? FilterAdded; public event EventHandler? FilterUpdated; public event EventHandler? FilterRemoved; public event EventHandler? OnConnectionChange; public async Task ConnectAsync(StoredUser? user = null) { if (_hubConnection != null) { await _hubConnection.DisposeAsync(); _hubConnection = null; } user ??= session.Current; if (user == null) return; _hubConnection = new HubConnectionBuilder() .WithUrl(navigation.ToAbsoluteUri("/hubs/streaming"), Auth) .WithAutomaticReconnect() .WithStatefulReconnect() .AddMessagePackProtocol() .Build(); _hub = _hubConnection.CreateHubProxy(); _hubConnection.Register(new StreamingHubClient(this)); try { await _hubConnection.StartAsync(); await _hub.SubscribeAsync(StreamingTimeline.Home); } catch (Exception e) { OnConnectionChange?.Invoke(this, HubConnectionState.Disconnected); logger.LogError("Connection failed: {error}", e.Message); } return; void Auth(HttpConnectionOptions options) => options.AccessTokenProvider = () => Task.FromResult(user.Token); } public async Task ReconnectAsync(StoredUser? user = null) { if (_hubConnection is null) { await ConnectAsync(user); return; } if (_hubConnection.State is not HubConnectionState.Disconnected) return; await _hubConnection.StartAsync(); } private class StreamingHubClient(StreamingService streaming) : IStreamingHubClient, IHubConnectionObserver { public Task OnClosed(Exception? exception) { streaming.OnConnectionChange?.Invoke(this, HubConnectionState.Disconnected); return Task.CompletedTask; } public Task OnReconnected(string? connectionId) { streaming.OnConnectionChange?.Invoke(this, HubConnectionState.Connected); return Task.CompletedTask; } public Task OnReconnecting(Exception? exception) { streaming.OnConnectionChange?.Invoke(this, HubConnectionState.Reconnecting); return Task.CompletedTask; } public Task NotificationAsync(NotificationResponse notification) { streaming.Notification?.Invoke(this, notification); return Task.CompletedTask; } public Task NotePublishedAsync(List timelines, NoteResponse note) { foreach (var timeline in timelines) streaming.NotePublished?.Invoke(this, (timeline, note)); return Task.CompletedTask; } public Task NoteUpdatedAsync(NoteResponse note) { streaming.NoteUpdated?.Invoke(this, note); return Task.CompletedTask; } public Task NoteDeletedAsync(string noteId) { streaming.NoteDeleted?.Invoke(this, noteId); return Task.CompletedTask; } public Task FilterAddedAsync(FilterResponse filter) { streaming.FilterAdded?.Invoke(this, filter); return Task.CompletedTask; } public Task FilterUpdatedAsync(FilterResponse filter) { streaming.FilterUpdated?.Invoke(this, filter); return Task.CompletedTask; } public Task FilterRemovedAsync(long filterId) { streaming.FilterRemoved?.Invoke(this, filterId); return Task.CompletedTask; } } }