diff --git a/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs index 165ff881b7..4e7ab74ffb 100644 --- a/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs +++ b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs @@ -13,8 +13,6 @@ public static IServiceCollection AddFwDataBridge(this IServiceCollection service services.AddLogging(); services.AddSingleton(); 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 80b872a8f7..b9672dadfe 100644 --- a/backend/FwDataMiniLcmBridge/FwDataFactory.cs +++ b/backend/FwDataMiniLcmBridge/FwDataFactory.cs @@ -64,9 +64,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() @@ -90,4 +93,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 72c600fdb1..3634d5f36d 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 039f1b929e..9d1e887fc4 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() diff --git a/backend/LocalWebApp/appsettings.Development.json b/backend/LocalWebApp/appsettings.Development.json index 960be50673..cd58546495 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/App.svelte b/frontend/viewer/src/App.svelte index ccfca65184..60aeae4f93 100644 --- a/frontend/viewer/src/App.svelte +++ b/frontend/viewer/src/App.svelte @@ -4,6 +4,7 @@ import TestProjectView from './TestProjectView.svelte'; import FwDataProjectView from './FwDataProjectView.svelte'; import HomeView from './HomeView.svelte'; + import NotificationOutlet from './lib/notifications/NotificationOutlet.svelte'; import Sandbox from './lib/sandbox/Sandbox.svelte'; export let url = ''; @@ -38,3 +39,4 @@ + diff --git a/frontend/viewer/src/FwDataProjectView.svelte b/frontend/viewer/src/FwDataProjectView.svelte index 3b7fff518a..30f9232aad 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/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index 1ae31435d7..cf0f7fd4be 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 0000000000..e30db017bd --- /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') {} +} diff --git a/frontend/viewer/src/lib/services/service-provider-signalr.ts b/frontend/viewer/src/lib/services/service-provider-signalr.ts index 8e2b7812ae..5c4319cb65 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); }