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<ReadyResponse>();
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
<DebugBanner Name="@GetType().Name" @ref="_dbgBanner"/>
-@if (_sessionPickerVisible) {
+@if (_sessionPickerVisible)
+{
<ModalWindow Title="Session manager" OnCloseClickedAsync="@(() => SetSessionPickerVisible(false))">
- @foreach (var (sessionId, session) in _sessionEntries) {
+ @foreach (var (sessionId, session) in _sessionEntries)
+ {
<LinkButton
OnClickAsync="@(() => SetCurrentSessionAsync(sessionId))">@(session.ProfileCache?.Username ?? "Unknown user")#@(session.ProfileCache?.Discriminator ?? "0000") <sub
class="code">on @session.ServerName</sub></LinkButton>
+ <LinkButton OnClickAsync="@(() => RemoveSessionAsync(sessionId))">x</LinkButton>
}
<br/><br/>
<LinkButton href="/login">Log in</LinkButton>
@@ -24,7 +29,8 @@
private Dictionary<Guid, SessionEntry> _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 @@
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
+ <UseSystemResourceKeys>false</UseSystemResourceKeys>
+ <UseSizeOptimizedLinq>false</UseSizeOptimizedLinq> <!-- makes Linq slightly faster -->
+ <DebuggerSupport>true</DebuggerSupport>
+ <TrimmerRemoveSymbols>false</TrimmerRemoveSymbols>
<!-- copied from https://cgit.rory.gay/matrix/tools/MatrixUtils.git/tree/MatrixUtils.Web/MatrixUtils.Web.csproj -->
<LinkIncremental>true</LinkIncremental>
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<SessionStore> 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<Guid>(CurrentSessionKey) == sessionId)
+ await localStorage.RemoveItemAsync(CurrentSessionKey);
+ }
}
public class SessionEntry {
|