From 90839fbb40dd754808e9a9dc41a9622b2a67cd43 Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 4 Sep 2026 12:35:33 +0200 Subject: [PATCH] client: gateway retry, session remove --- .../Components/ClientManager.razor | 95 ++++++++++++++----- .../Components/SessionManager.razor | 39 +++++++- .../Spacebar.Client/Spacebar.Client.csproj | 4 + .../Spacebar.Client/WebCore/SessionStore.cs | 11 +++ 4 files changed, 122 insertions(+), 27 deletions(-) diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor b/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor index 2021b8d22..e7835d5b6 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor @@ -1,7 +1,9 @@ +@using System.Diagnostics @using ArcaneLibs.Blazor.Components.Services @using ArcaneLibs.Extensions @using Spacebar.Client.WebCore @using Spacebar.Client.WebCore.Client +@using Spacebar.Models.Api @using Spacebar.Models.Gateway @using Spacebar.Sdk.Core @inject SessionStore sessionStore @@ -17,12 +19,15 @@ private DebugBanner _dbgBanner = null!; private bool _readyReceived = false; - public ClientManager() { - ClientAvailable = Task.Run(async () => { + public ClientManager() + { + ClientAvailable = Task.Run(async () => + { while (Client is null) await Task.Delay(50); ClientAvailable = null; }); - ClientReady = Task.Run(async () => { + ClientReady = Task.Run(async () => + { while (!_readyReceived) await Task.Delay(50); ClientAvailable = null; }); @@ -38,40 +43,40 @@ public Task? ClientAvailable { get; set; } public Task? ClientReady { get; set; } - protected override async Task OnAfterRenderAsync(bool firstRender) { + protected override async Task OnAfterRenderAsync(bool firstRender) + { if (!firstRender) return; await _dbgBanner.SetStatus("Preparing for launch!"); await Task.Delay(125); var session = await sessionStore.GetCurrentSessionAsync(); - if (session != null) { + if (session != null) + { Client = await clientProvider.GetAuthenticatedClientAsync(session.ServerName, session.AccessToken); await _dbgBanner.SetStatus($"Got authenticated client for {session.ProfileCache.Username}#{session.ProfileCache.Discriminator} on {session.ServerName}! Connecting to gateway..."); - Client.Gateway.IdentifyData.ClientProperties = new IdentifyClientProperties() { + Client.Gateway.IdentifyData.ClientProperties = new IdentifyClientProperties() + { HasClientMods = false, - ApplicationArchitecture = "wasm" + ApplicationArchitecture = "wasm", }.ToJsonNode().AsObject(); StateHasChanged(); - await Client.Gateway.Connect(); - _ = Client.Gateway.Start().ContinueWith(ct => { - jsConsole.Warn("[ClientManager] Heartbeat loop exited!"); - if (ct.IsFaulted) { - jsConsole.Error("Unhandled exception during gateway connection:", ct.Exception.ToString()); - throw ct.Exception; - } - }); - Client.Gateway.OnceGatewayMessage.Add(async msg => { - if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) { + _ = RunInnerLoopAsync(); + Client.Gateway.OnceGatewayMessage.Add(async msg => + { + if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) + { await _dbgBanner.SetStatus($"Got READY from gateway, deserializing..."); var content = msg.GetData(); await _dbgBanner.SetStatus($"Deserialized READY from gateway, handling..."); // ClientState.Guilds.AddRange(content.Guilds.ToDictionary(x=>x.Id, x=>x)); - foreach (var guild in content.Guilds) { + foreach (var guild in content.Guilds) + { ClientState.Guilds.Add(guild.Id, guild); await _dbgBanner.SetStatus($"Deserialized READY from gateway, handling... guilds ({ClientState.Guilds.Count})"); await Task.Delay(1); } - foreach (var guild in content.Relationships) { + foreach (var guild in content.Relationships) + { // ClientState.Relationships.Add(guild.Id, guild); await _dbgBanner.SetStatus($"Deserialized READY from gateway, handling... guilds ({ClientState.Guilds.Count}), relationships (0)"); await Task.Delay(1); @@ -84,20 +89,66 @@ return false; } - if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY_SUPPLEMENTAL" }) { + if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY_SUPPLEMENTAL" }) + { await _dbgBanner.SetStatus("Received READY_SUPPLEMENTAL..."); await jsConsole.Info("Parsed ready_supplemental payload", new { original = msg.EventData }); - _ =_dbgBanner.SetStatus(null, 1750); + _ = _dbgBanner.SetStatus(null, 1750); return true; } return false; }); } - else { + else + { await _dbgBanner.SetStatus("No session marked as current... :("); await _dbgBanner.SetStatus(null, 1750); } } + private async Task RunInnerLoopAsync() + { + var tries = 1; + while (true) + { + var uptime = Stopwatch.StartNew(); + try + { + int delay = (int)((tries++ * 1.2) * 250); + await jsConsole.Info("Starting gateway connection in", delay, "ms."); + await Task.Delay(delay); + await Client.Gateway.Connect(); + await Client.Gateway.Start(); + } + catch (Exception e) + { + await Client.Gateway.Disconnect(); + await jsConsole.Warn($"[ClientManager] Heartbeat loop exited! ({Client.Gateway.RawClientWebSocket.CloseStatus}: {Client.Gateway.RawClientWebSocket.CloseStatusDescription})"); + await jsConsole.Error("Unhandled exception during gateway connection:", e.ToString()); + _ = _dbgBanner.SetStatus("Unhandled exception during gateway connection: " + string.Join("\n", e.ToString().Split("\n").Where(x => !x.Trim().StartsWith("at ")))); + if (uptime.Elapsed.TotalSeconds < 3) + { + try + { + await _dbgBanner.SetStatus("Attempting to validate token..."); + await Client.GetCurrentUser(); + } + catch (Exception ve) + { + await _dbgBanner.SetStatus(ve.ToString()); + await jsConsole.Error(ve.ToString()); + if (ve is SpacebarApiException { Code: 40001 }) + { + await _dbgBanner.SetStatus("Invalid access token, please sign in again!"); + return; + } + } + } + await Task.Delay(1); + } + } + // ReSharper disable once FunctionNeverReturns + } + } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/SessionManager.razor b/extra/admin-api/Utilities/Spacebar.Client/Components/SessionManager.razor index 0c9a10644..3162ea354 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Components/SessionManager.razor +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/SessionManager.razor @@ -1,16 +1,21 @@ @using ArcaneLibs.Blazor.Components @using ArcaneLibs.Blazor.Components.Services @using Spacebar.Client.WebCore +@using Spacebar.Sdk.Core @inject SessionStore sessionStore +@inject SpacebarClientProviderService clientProvider @inject JsConsoleService jsConsole -@if (_sessionPickerVisible) { +@if (_sessionPickerVisible) +{ - @foreach (var (sessionId, session) in _sessionEntries) { + @foreach (var (sessionId, session) in _sessionEntries) + { @(session.ProfileCache?.Username ?? "Unknown user")#@(session.ProfileCache?.Discriminator ?? "0000") on @session.ServerName + x }

Log in @@ -24,7 +29,8 @@ private Dictionary _sessionEntries = []; - protected override async Task OnAfterRenderAsync(bool firstRender) { + protected override async Task OnAfterRenderAsync(bool firstRender) + { if (!firstRender) return; await _dbgBanner.SetStatus("Crunching numbers..."); _sessionPickerVisible = await sessionStore.GetCurrentSessionAsync() == null; @@ -32,7 +38,8 @@ await _dbgBanner.SetStatus(null, 1000); } - public async Task SetSessionPickerVisible(bool visible = true) { + public async Task SetSessionPickerVisible(bool visible = true) + { await jsConsole.Info("[SessionManager] showing session picker..."); _sessionEntries = visible ? await sessionStore.GetAllSessionsAsync() : []; _sessionPickerVisible = visible; @@ -40,7 +47,8 @@ await Task.Delay(1); } - private async Task SetCurrentSessionAsync(Guid sessionId) { + private async Task SetCurrentSessionAsync(Guid sessionId) + { _sessionPickerVisible = false; StateHasChanged(); await sessionStore.SetCurrentSessionAsync(sessionId); @@ -49,4 +57,25 @@ await _dbgBanner.SetStatus(null, 3000); } + private async Task RemoveSessionAsync(Guid sessionId) + { + try + { + await _dbgBanner.SetStatus("Trying to log out..."); + var sess = _sessionEntries[sessionId]; + var client = await clientProvider.GetAuthenticatedClientAsync(sess.ServerName, sess.AccessToken); + await client.LogOutAsync(); + } + catch (Exception e) + { + await _dbgBanner.SetStatus("Failed to log out, removing anyway: " + string.Join("\n", e.ToString().Split("\n").Where(x => !x.Trim().StartsWith("at ")))); + await Task.Delay(1000); + } + + await sessionStore.RemoveSessionAsync(sessionId); + await _dbgBanner.SetStatus("Session removed successfully!"); + await _dbgBanner.SetStatus(null, 1000); + await SetSessionPickerVisible(); + } + } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj b/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj index f4d74604b..6042f3ccb 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj +++ b/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj @@ -6,6 +6,10 @@ enable true service-worker-assets.js + false + false + true + false true diff --git a/extra/admin-api/Utilities/Spacebar.Client/WebCore/SessionStore.cs b/extra/admin-api/Utilities/Spacebar.Client/WebCore/SessionStore.cs index ae98ee570..5f982fa69 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/WebCore/SessionStore.cs +++ b/extra/admin-api/Utilities/Spacebar.Client/WebCore/SessionStore.cs @@ -33,6 +33,17 @@ public class SessionStore(ILogger logger, LocalStorageService loca public async Task SetCurrentSessionAsync(Guid sessionId) { await localStorage.SetItemAsJsonAsync(CurrentSessionKey, sessionId); } + + public async Task RemoveSessionAsync(Guid sessionId) + { + var sessions = await GetAllSessionsAsync(); + sessions.Remove(sessionId); + await localStorage.SetItemAsJsonAsync(AllSessionsKey, sessions); + + if (await localStorage.ContainsKeyAsync(CurrentSessionKey)) + if (await localStorage.GetItemFromJsonAsync(CurrentSessionKey) == sessionId) + await localStorage.RemoveItemAsync(CurrentSessionKey); + } } public class SessionEntry {