From b4b35c8f8e8a5019c1d72510a96e515baeb65e26 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 4 Jul 2024 13:22:26 +0700 Subject: [PATCH 1/5] close fwdata project when the client disconnects, notify all clients in the group that the project is closed. --- .../FwDataMiniLcmBridge/FwDataBridgeKernel.cs | 2 -- backend/FwDataMiniLcmBridge/FwDataFactory.cs | 22 +++++++++++++++++-- backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs | 1 + backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs | 22 ++++++++++++++++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs index 59d8de9ac..b30a7b273 100644 --- a/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs +++ b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs @@ -11,8 +11,6 @@ public static IServiceCollection AddFwDataBridge(this IServiceCollection service { services.AddMemoryCache(); services.AddSingleton(); - //todo since this is scoped it gets created on each request (or hub method call), which opens the project file on each request - //this is not ideal since opening the project file can be slow. It should be done once per hub connection. services.AddKeyedScoped(FwDataApiKey, (provider, o) => provider.GetRequiredService().GetCurrentFwDataMiniLcmApi(true)); services.AddSingleton(); return services; diff --git a/backend/FwDataMiniLcmBridge/FwDataFactory.cs b/backend/FwDataMiniLcmBridge/FwDataFactory.cs index 16497de43..a807bf221 100644 --- a/backend/FwDataMiniLcmBridge/FwDataFactory.cs +++ b/backend/FwDataMiniLcmBridge/FwDataFactory.cs @@ -59,9 +59,12 @@ private static void OnLcmProjectCacheEviction(object key, object? value, Evictio var (logger, projects) = ((ILogger, HashSet))state!; var name = lcmCache.ProjectId.Name; logger.LogInformation("Evicting project {ProjectFileName} from cache", name); - lcmCache.Dispose(); - logger.LogInformation("FW Data Project {ProjectFileName} disposed", name); projects.Remove((string)key); + if (!lcmCache.IsDisposed) + { + lcmCache.Dispose(); + logger.LogInformation("FW Data Project {ProjectFileName} disposed", name); + } } public void Dispose() @@ -85,4 +88,19 @@ public FwDataMiniLcmApi GetCurrentFwDataMiniLcmApi(bool saveOnDispose) } return GetFwDataMiniLcmApi(fwDataProject, true); } + + public void CloseCurrentProject() + { + var fwDataProject = context.Project; + if (fwDataProject is null) return; + CloseProject(fwDataProject); + } + + private void CloseProject(FwDataProject project) + { + var cacheKey = CacheKey(project); + var lcmCache = cache.Get(cacheKey); + if (lcmCache is null) return; + cache.Remove(cacheKey); + } } diff --git a/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs b/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs index 72c600fdb..3634d5f36 100644 --- a/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs +++ b/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs @@ -9,6 +9,7 @@ namespace LocalWebApp.Hubs; public interface ILexboxClient { Task OnEntryUpdated(Entry entry); + Task OnProjectClosed(); } public class CrdtMiniLcmApiHub( diff --git a/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs b/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs index 3c5527320..da6e0813e 100644 --- a/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs +++ b/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs @@ -6,11 +6,31 @@ namespace LocalWebApp.Hubs; -public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi) : Hub +public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi, FwDataFactory fwDataFactory, + FwDataProjectContext context) : Hub { public const string ProjectRouteKey = "fwdata"; public override async Task OnConnectedAsync() { + var project = context.Project; + if (project is null) + { + throw new InvalidOperationException("No project is set in the context."); + } + await Groups.AddToGroupAsync(Context.ConnectionId, project.Name); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + //todo if multiple clients are connected, this will close the project for all of them. + fwDataFactory.CloseCurrentProject(); + var project = context.Project; + if (project is null) + { + throw new InvalidOperationException("No project is set in the context."); + } + await Clients.OthersInGroup(project.Name).OnProjectClosed(); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, project.Name); } public async Task GetWritingSystems() From f96a8067710c037017b7a080bcd8f52b2c8a7923 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 4 Jul 2024 16:08:09 +0700 Subject: [PATCH 2/5] add button to navigate home from project view --- frontend/viewer/src/ProjectView.svelte | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/viewer/src/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index d8ea67ca4..dbd106235 100644 --- a/frontend/viewer/src/ProjectView.svelte +++ b/frontend/viewer/src/ProjectView.svelte @@ -1,7 +1,8 @@ +
+ {#each $notifications as notification} +
+ +
+ {#if notification.type === 'success'} + + {:else if notification.type === 'error'} + + {:else if notification.type === 'info'} + + {:else if notification.type === 'warning'} + + {/if} +
+
{notification.message}
+
+
+ {/each} +
diff --git a/frontend/viewer/src/lib/notifications/notifications.ts b/frontend/viewer/src/lib/notifications/notifications.ts new file mode 100644 index 000000000..e30db017b --- /dev/null +++ b/frontend/viewer/src/lib/notifications/notifications.ts @@ -0,0 +1,21 @@ +import {writable, type Writable, type Readable, readonly} from 'svelte/store'; + +export class AppNotification { + private static _notifications: Writable = writable([]); + public static get notifications(): Writable { + return this._notifications; + } + public static display(message: string, type: 'success' | 'error' | 'info' | 'warning', timeout: 'short' | 'long' | number = 'short') { + const notification = new AppNotification(message, type); + this._notifications.update(notifications => [...notifications, notification]); + if (timeout === -1) return; + if (typeof timeout === 'string') { + timeout = timeout === 'short' ? 5000 : 30000; + } + setTimeout(() => { + this._notifications.update(notifications => notifications.filter(n => n !== notification)); + }, timeout); + } + + private constructor(public message: string, public type: 'success' | 'error' | 'info' | 'warning') {} +} From 9d875d24986ca6f3926e7ab18d7f3f04d431d955 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 4 Jul 2024 16:09:05 +0700 Subject: [PATCH 4/5] only log auth warnings --- backend/LocalWebApp/appsettings.Development.json | 3 ++- frontend/viewer/src/FwDataProjectView.svelte | 6 ++++++ .../TypedSignalR.Client/Lexbox.ClientServer.Hubs.ts | 1 + .../generated-signalr-client/TypedSignalR.Client/index.ts | 5 ++++- .../viewer/src/lib/services/service-provider-signalr.ts | 6 ++++-- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/LocalWebApp/appsettings.Development.json b/backend/LocalWebApp/appsettings.Development.json index 960be5067..cd5854649 100644 --- a/backend/LocalWebApp/appsettings.Development.json +++ b/backend/LocalWebApp/appsettings.Development.json @@ -1,7 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information" + "Default": "Information", + "LocalWebApp.Auth.LoggerAdapter": "Warning", } } } diff --git a/frontend/viewer/src/FwDataProjectView.svelte b/frontend/viewer/src/FwDataProjectView.svelte index 3b7fff518..30f9232aa 100644 --- a/frontend/viewer/src/FwDataProjectView.svelte +++ b/frontend/viewer/src/FwDataProjectView.svelte @@ -4,6 +4,8 @@ import {onDestroy, setContext} from 'svelte'; import {SetupSignalR} from './lib/services/service-provider-signalr'; import ProjectView from './ProjectView.svelte'; + import {navigate} from 'svelte-routing'; + import {AppNotification} from './lib/notifications/notifications'; export let projectName: string; const connection = new HubConnectionBuilder() @@ -18,6 +20,10 @@ SetupSignalR(connection, { history: false, write: true, + }, + async () => { + navigate('/'); + AppNotification.display('Project closed on another tab', 'warning', 'long'); }); let connected = false; diff --git a/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/Lexbox.ClientServer.Hubs.ts b/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/Lexbox.ClientServer.Hubs.ts index 3c174506e..7800cf680 100644 --- a/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/Lexbox.ClientServer.Hubs.ts +++ b/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/Lexbox.ClientServer.Hubs.ts @@ -13,5 +13,6 @@ export type ILexboxClient = { * @returns Transpiled from System.Threading.Tasks.Task */ OnEntryUpdated(entry: Entry): Promise; + OnProjectClosed(): Promise; } diff --git a/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/index.ts b/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/index.ts index acb80396d..539a30ca0 100644 --- a/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/index.ts +++ b/frontend/viewer/src/lib/generated-signalr-client/TypedSignalR.Client/index.ts @@ -169,11 +169,14 @@ class ILexboxClient_Binder implements ReceiverRegister { public readonly register = (connection: HubConnection, receiver: ILexboxClient): Disposable => { const __onEntryUpdated = (...args: [Entry]) => receiver.OnEntryUpdated(...args); + const __onProjectClosed = () => receiver.OnProjectClosed(); connection.on("OnEntryUpdated", __onEntryUpdated); + connection.on("OnProjectClosed", __onProjectClosed); const methodList: ReceiverMethod[] = [ - { methodName: "OnEntryUpdated", method: __onEntryUpdated } + { methodName: "OnEntryUpdated", method: __onEntryUpdated }, + { methodName: "OnProjectClosed", method: __onProjectClosed }, ] return new ReceiverMethodSubscription(connection, methodList); diff --git a/frontend/viewer/src/lib/services/service-provider-signalr.ts b/frontend/viewer/src/lib/services/service-provider-signalr.ts index 8e2b7812a..5c4319cb6 100644 --- a/frontend/viewer/src/lib/services/service-provider-signalr.ts +++ b/frontend/viewer/src/lib/services/service-provider-signalr.ts @@ -5,7 +5,8 @@ import type { HubConnection } from '@microsoft/signalr'; import type { LexboxApiFeatures, LexboxApiMetadata } from './lexbox-api'; import {LexboxService} from './service-provider'; -export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatures) { +const noop = () => Promise.resolve(); +export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatures, onProjectClosed: () => Promise = noop) { const hubFactory = getHubProxyFactory('ILexboxApiHub'); const hubProxy = hubFactory.createHubProxy(connection); @@ -17,7 +18,8 @@ export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatu getReceiverRegister('ILexboxClient').register(connection, { OnEntryUpdated: async (entry: Entry) => { console.log('OnEntryUpdated', entry); - } + }, + OnProjectClosed: onProjectClosed }); window.lexbox.ServiceProvider.setService(LexboxService.LexboxApi, lexboxApiHubProxy); } From a90bf79f04ff061c4eaac429c385ddaed5664db4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 5 Jul 2024 17:01:47 +0700 Subject: [PATCH 5/5] fix bug where the home button wouldn't show correctly which was caused by dynamically rendering it with #if --- frontend/viewer/src/ProjectView.svelte | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/viewer/src/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index 5b691a754..cf0f7fd4b 100644 --- a/frontend/viewer/src/ProjectView.svelte +++ b/frontend/viewer/src/ProjectView.svelte @@ -231,13 +231,12 @@ {:else}
- {#if showHomeButton} -