From 2499cd6ad0831d16a243c7de173665d0d1b19f95 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 10 Jan 2025 16:21:53 +0700 Subject: [PATCH] Blazor cleanup (#1358) * rework DotnetProjectView.svelte to await a call to `miniLcmApiProvider` instead of polling the override services * clear mini lcm api provider when the DotnetProjectView is destroyed, also dispose of the api when clearing it from the provider * only shutdown hosted services on close instead of the whole maui app which caused errors on close * ensure on shutdown all FwData projects are properly closed * save fwdata changes on each API call * simplify tasks for fw lite, change Lexbox Local server to use oauth proxy port --- Taskfile.yml | 11 +- .../Api/FwDataMiniLcmApi.cs | 2 +- .../FwDataMiniLcmBridge/FwDataBridgeKernel.cs | 2 + .../FwDataMiniLcmBridge/FwDataFactory.cs | 19 ++- backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs | 7 +- backend/FwLite/FwLiteMaui/MauiProgram.cs | 8 +- .../FwLite/FwLiteShared/FwLiteSharedKernel.cs | 1 + .../FwLiteShared/Pages/FwdataProject.razor | 7 +- .../FwLiteShared/Services/FwLiteProvider.cs | 73 ++++++++- .../Services/MiniLcmJsInvokable.cs | 141 +++++++++++------- .../TypeGen/ReinforcedFwLiteTypingConfig.cs | 9 ++ backend/FwLite/FwLiteWeb/FwLiteWebServer.cs | 2 +- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 8 + backend/FwLite/Taskfile.yml | 14 +- frontend/viewer/src/DotnetProjectView.svelte | 30 ++-- .../FwLiteShared/Services/DotnetService.ts | 1 + .../Services/IMiniLcmApiProvider.ts | 14 ++ .../lib/services/service-provider-dotnet.ts | 8 +- .../src/lib/services/service-provider.ts | 11 ++ 19 files changed, 269 insertions(+), 99 deletions(-) create mode 100644 frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IMiniLcmApiProvider.ts diff --git a/Taskfile.yml b/Taskfile.yml index 8d296dcc5..aa3beba07 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -93,8 +93,11 @@ tasks: deps: [ infra-up, api:only, k8s:infra-forward ] interactive: true - web-for-develop: - deps: [ ui:viewer-dev, fw-lite:lweb-for-develop, ui:https-oauth-authority ] - - web: + fw-lite-web: + aliases: + - web + - web-for-develop deps: [ ui:viewer-dev, fw-lite:web, ui:https-oauth-authority ] + + fw-lite-win: + deps: [fw-lite:maui-windows, ui:viewer-dev, ui:https-oauth-authority] diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 80eebd829..42af56c24 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -19,7 +19,7 @@ namespace FwDataMiniLcmBridge.Api; -public class FwDataMiniLcmApi(Lazy cacheLazy, bool onCloseSave, ILogger logger, FwDataProject project, MiniLcmValidators validators) : IMiniLcmApi, IDisposable +public class FwDataMiniLcmApi(Lazy cacheLazy, bool onCloseSave, ILogger logger, FwDataProject project, MiniLcmValidators validators) : IMiniLcmApi, IMiniLcmSaveApi { internal LcmCache Cache => cacheLazy.Value; public FwDataProject Project { get; } = project; diff --git a/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs b/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs index cbeceb503..4e6287541 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs @@ -1,5 +1,6 @@ using FwDataMiniLcmBridge.LcmUtils; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using MiniLcm; using MiniLcm.Project; using MiniLcm.Validators; @@ -15,6 +16,7 @@ public static IServiceCollection AddFwDataBridge(this IServiceCollection service services.AddLogging(); services.AddOptions().BindConfiguration("FwDataBridge"); services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); services.AddSingleton(s => s.GetRequiredService()); services.AddSingleton(); diff --git a/backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs b/backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs index 64919b466..9ad88d285 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs @@ -14,7 +14,7 @@ public class FwDataFactory( IMemoryCache cache, ILogger logger, IProjectLoader projectLoader, - MiniLcmValidators validators) : IDisposable + MiniLcmValidators validators) : IDisposable, IHostedService { private bool _shuttingDown = false; public FwDataFactory(ILogger fwdataLogger, @@ -87,13 +87,16 @@ private static void OnLcmProjectCacheEviction(object key, object? value, Evictio public void Dispose() { logger.LogInformation("Closing all projects"); - foreach (var project in _projects) + //ensure a race condition doesn't cause us to dispose of a project that's already been disposed + var projects = Interlocked.Exchange(ref _projects, []); + foreach (var project in projects) { var lcmCache = cache.Get(project); if (lcmCache is null || lcmCache.IsDisposed) continue; var name = lcmCache.ProjectId.Name; lcmCache.Dispose(); //need to explicitly call dispose as that blocks, just removing from the cache does not block, meaning it will not finish disposing before the program exits. logger.LogInformation("FW Data Project {ProjectFileName} disposed", name); + cache.Remove(project); } } @@ -112,4 +115,16 @@ public IDisposable DeferClose(FwDataProject project) { return Defer.Action(() => CloseProject(project)); } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + //Services in MAUI apps aren't disposed when the app is shut down, we have a workaround to shutdown HostedServices on shutdown, so we made this IHostedService to close projects on shutdown + public Task StopAsync(CancellationToken cancellationToken) + { + _shuttingDown = true; + return Task.Run(Dispose, cancellationToken); + } } diff --git a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs index 61dc04d22..677381952 100644 --- a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs +++ b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs @@ -34,7 +34,8 @@ public static void AddFwLiteMauiServices(this IServiceCollection services, services.AddSingleton(env); services.AddFwLiteShared(env); services.AddMauiBlazorWebView(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); #if INCLUDE_FWDATA_BRIDGE //need to call them like this otherwise we need a using statement at the top of the file FwDataMiniLcmBridge.FwDataBridgeKernel.AddFwDataBridge(services); @@ -127,7 +128,7 @@ public static void AddFwLiteMauiServices(this IServiceCollection services, public static bool IsPortableApp => false; #endif - private class HostedServiceAdapter(IEnumerable hostedServices, ILogger logger) : IMauiInitializeService, IAsyncDisposable + internal class HostedServiceAdapter(IEnumerable hostedServices, ILogger logger) : IMauiInitializeService, IAsyncDisposable { private CancellationTokenSource _cts = new(); public void Initialize(IServiceProvider services) @@ -141,8 +142,8 @@ public void Initialize(IServiceProvider services) public async ValueTask DisposeAsync() { - //todo this is never called because the service provider is not disposed logger.LogInformation("Disposing hosted services"); + //todo this should probably have a timeout so we don't hang forever foreach (var hostedService in hostedServices) { await hostedService.StopAsync(_cts.Token); diff --git a/backend/FwLite/FwLiteMaui/MauiProgram.cs b/backend/FwLite/FwLiteMaui/MauiProgram.cs index 307ed8076..a84e5f26c 100644 --- a/backend/FwLite/FwLiteMaui/MauiProgram.cs +++ b/backend/FwLite/FwLiteMaui/MauiProgram.cs @@ -16,14 +16,16 @@ public void Shutdown() if (App == null) return; var logger = App.Services.GetRequiredService>(); + var adapter = App.Services.GetRequiredService(); try { - logger.LogInformation("Disposing app"); - App.DisposeAsync().GetAwaiter().GetResult(); + logger.LogInformation("Disposing hosted services"); + //I tried to dispose of the app, but that caused other issues on shutdown, so we're just going to dispose of the hosted services + adapter.DisposeAsync().GetAwaiter().GetResult(); } catch (Exception e) { - logger.LogError(e, "Failed to dispose app"); + logger.LogError(e, "Failed to dispose hosted services"); throw; } }) diff --git a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs index 9c0af4153..5927b79ff 100644 --- a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs +++ b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs @@ -15,6 +15,7 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service services.AddHttpClient(); services.AddAuthHelpers(environment); services.AddLcmCrdtClient(); + services.AddLogging(); services.AddSingleton(); services.AddScoped(); diff --git a/backend/FwLite/FwLiteShared/Pages/FwdataProject.razor b/backend/FwLite/FwLiteShared/Pages/FwdataProject.razor index 7dfbe564c..c002ec062 100644 --- a/backend/FwLite/FwLiteShared/Pages/FwdataProject.razor +++ b/backend/FwLite/FwLiteShared/Pages/FwdataProject.razor @@ -13,11 +13,12 @@ private IAsyncDisposable? _disposable; - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override Task OnAfterRenderAsync(bool firstRender) { - if (!firstRender) return; + if (!firstRender) return Task.CompletedTask; var fwLiteProvider = ScopedServices.GetRequiredService(); - _disposable = await fwLiteProvider.InjectFwDataProject(JS, ScopedServices, ProjectName); + _disposable = fwLiteProvider.InjectFwDataProject(ScopedServices, ProjectName); + return Task.CompletedTask; } protected override async ValueTask DisposeAsyncCore() diff --git a/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs b/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs index f862d15bb..8584837f3 100644 --- a/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs +++ b/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.JSInterop; +using MiniLcm; using MiniLcm.Models; using MiniLcm.Project; @@ -22,11 +23,13 @@ public class FwLiteProvider( ChangeEventBus changeEventBus, IEnumerable projectProviders, ILogger logger, - IOptions config + IOptions config, + ILoggerFactory loggerFactory ) : IDisposable { public const string OverrideServiceFunctionName = "setOverrideService"; private readonly List _disposables = []; + private readonly MiniLcmApiProvider _miniLcmApiProvider = new(loggerFactory.CreateLogger()); private IProjectProvider? FwDataProjectProvider => projectProviders.FirstOrDefault(p => p.DataFormat == ProjectDataFormat.FwData); @@ -53,6 +56,7 @@ public object GetService(DotnetService service) DotnetService.AuthService => authService, DotnetService.ImportFwdataService => importFwdataService, DotnetService.FwLiteConfig => config.Value, + DotnetService.MiniLcmApiProvider => _miniLcmApiProvider, _ => throw new ArgumentOutOfRangeException(nameof(service), service, null) }; } @@ -95,36 +99,91 @@ public async Task InjectCrdtProject(IJSRuntime jsRuntime, { _ = jsRuntime.DurableInvokeVoidAsync("notifyEntryUpdated", projectName, entry); }); - var service = ActivatorUtilities.CreateInstance(scopedServices, project); - var reference = await SetService(jsRuntime,DotnetService.MiniLcmApi, service); + var cleanup = ProvideMiniLcmApi(ActivatorUtilities.CreateInstance(scopedServices, project)); + return Defer.Async(() => { - reference?.Dispose(); + cleanup.Dispose(); entryUpdatedSubscription.Dispose(); return Task.CompletedTask; }); } - public async Task InjectFwDataProject(IJSRuntime jsRuntime, IServiceProvider scopedServices, string projectName) + public IAsyncDisposable InjectFwDataProject(IServiceProvider scopedServices, string projectName) { if (FwDataProjectProvider is null) throw new InvalidOperationException("FwData Project provider is not available"); var project = FwDataProjectProvider.GetProject(projectName) ?? throw new InvalidOperationException($"FwData Project {projectName} not found"); var service = ActivatorUtilities.CreateInstance(scopedServices, FwDataProjectProvider.OpenProject(project), project); - var reference = await SetService(jsRuntime, DotnetService.MiniLcmApi, service); + var cleanup = ProvideMiniLcmApi(service); return Defer.Async(() => { - reference?.Dispose(); + cleanup.Dispose(); service.Dispose(); return Task.CompletedTask; }); } + + private IDisposable ProvideMiniLcmApi(MiniLcmJsInvokable miniLcmApi) + { + var reference = DotNetObjectReference.Create(miniLcmApi); + _miniLcmApiProvider.SetMiniLcmApi(reference); + return Defer.Action(() => + { + reference?.Dispose(); + _miniLcmApiProvider.ClearMiniLcmApi(); + }); + } +} + +/// +/// this service is used to allow the frontend to await the api being setup by the backend, this means the frontend doesn't need to poll for the api being ready +/// +internal class MiniLcmApiProvider(ILogger logger) +{ + private TaskCompletionSource> _tcs = new(); + + [JSInvokable] + public async Task> GetMiniLcmApi() + { +#pragma warning disable VSTHRD003 + return await _tcs.Task; +#pragma warning restore VSTHRD003 + } + + public void SetMiniLcmApi(DotNetObjectReference miniLcmApi) + { + logger.LogInformation("Setting MiniLcmApi"); + _tcs.SetResult(miniLcmApi); + } + + [JSInvokable] + public void ClearMiniLcmApi() + { + logger.LogInformation("Clearing MiniLcmApi"); + //we can't cancel a tcs if it's already completed + if (!_tcs.Task.IsCompleted) + { + //we need to tell any clients awaiting the tcs it's canceled. otherwise they will hang + _tcs.SetCanceled(); + } + else if (_tcs.Task.IsCompletedSuccessfully) + { +#pragma warning disable VSTHRD002 + _tcs.Task.Result.Value.Dispose(); +#pragma warning restore VSTHRD002 + } + + //create a new tcs so any new clients will await the new api. + _tcs = new(); + } } [JsonConverter(typeof(JsonStringEnumConverter))] public enum DotnetService { MiniLcmApi, + MiniLcmApiProvider, CombinedProjectsService, AuthService, ImportFwdataService, diff --git a/backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs b/backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs index 6e89e6ecc..d24f7a3c4 100644 --- a/backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs +++ b/backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs @@ -22,8 +22,12 @@ public MiniLcmFeatures SupportedFeatures() return new(History: isCrdtProject, Write: true, OpenWithFlex: isFwDataProject, Feedback: true, Sync: SupportsSync); } - private void TriggerSync() + private void OnDataChanged() { + if (api is IMiniLcmSaveApi saveApi) + { + saveApi.Save(); + } if (SupportsSync) { backgroundSyncService.TriggerSync(project); @@ -109,69 +113,88 @@ public Task CreateWritingSystem(WritingSystemType type, WritingSy } [JSInvokable] - public Task UpdateWritingSystem(WritingSystem before, WritingSystem after) + public async Task UpdateWritingSystem(WritingSystem before, WritingSystem after) { - return api.UpdateWritingSystem(before, after); + var updatedWritingSystem = await api.UpdateWritingSystem(before, after); + OnDataChanged(); + return updatedWritingSystem; } [JSInvokable] - public Task CreatePartOfSpeech(PartOfSpeech partOfSpeech) + public async Task CreatePartOfSpeech(PartOfSpeech partOfSpeech) { - return api.CreatePartOfSpeech(partOfSpeech); + var createdPartOfSpeech = await api.CreatePartOfSpeech(partOfSpeech); + OnDataChanged(); + return createdPartOfSpeech; } [JSInvokable] - public Task UpdatePartOfSpeech(PartOfSpeech before, PartOfSpeech after) + public async Task UpdatePartOfSpeech(PartOfSpeech before, PartOfSpeech after) { - return api.UpdatePartOfSpeech(before, after); + var updatedPartOfSpeech = await api.UpdatePartOfSpeech(before, after); + OnDataChanged(); + return updatedPartOfSpeech; } [JSInvokable] - public Task DeletePartOfSpeech(Guid id) + public async Task DeletePartOfSpeech(Guid id) { - return api.DeletePartOfSpeech(id); + await api.DeletePartOfSpeech(id); + OnDataChanged(); } [JSInvokable] - public Task CreateSemanticDomain(SemanticDomain semanticDomain) + public async Task CreateSemanticDomain(SemanticDomain semanticDomain) { - return api.CreateSemanticDomain(semanticDomain); + var createdSemanticDomain = await api.CreateSemanticDomain(semanticDomain); + OnDataChanged(); + return createdSemanticDomain; } [JSInvokable] - public Task UpdateSemanticDomain(SemanticDomain before, SemanticDomain after) + public async Task UpdateSemanticDomain(SemanticDomain before, SemanticDomain after) { - return api.UpdateSemanticDomain(before, after); + var updatedSemanticDomain = await api.UpdateSemanticDomain(before, after); + OnDataChanged(); + return updatedSemanticDomain; } [JSInvokable] - public Task DeleteSemanticDomain(Guid id) + public async Task DeleteSemanticDomain(Guid id) { - return api.DeleteSemanticDomain(id); + await api.DeleteSemanticDomain(id); + OnDataChanged(); } [JSInvokable] - public Task CreateComplexFormType(ComplexFormType complexFormType) + public async Task CreateComplexFormType(ComplexFormType complexFormType) { - return api.CreateComplexFormType(complexFormType); + var createdComplexFormType = await api.CreateComplexFormType(complexFormType); + OnDataChanged(); + return createdComplexFormType; } [JSInvokable] - public Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after) + public async Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after) { - return api.UpdateComplexFormType(before, after); + var updatedComplexFormType = await api.UpdateComplexFormType(before, after); + OnDataChanged(); + return updatedComplexFormType; } [JSInvokable] - public Task DeleteComplexFormType(Guid id) + public async Task DeleteComplexFormType(Guid id) { - return api.DeleteComplexFormType(id); + await api.DeleteComplexFormType(id); + OnDataChanged(); } [JSInvokable] - public Task CreateEntry(Entry entry) + public async Task CreateEntry(Entry entry) { - return api.CreateEntry(entry); + var createdEntry = await api.CreateEntry(entry); + OnDataChanged(); + return createdEntry; } [JSInvokable] @@ -179,86 +202,104 @@ public async Task UpdateEntry(Entry before, Entry after) { //todo trigger sync on the test var result = await api.UpdateEntry(before, after); - TriggerSync(); + OnDataChanged(); return result; } [JSInvokable] - public Task DeleteEntry(Guid id) + public async Task DeleteEntry(Guid id) { - return api.DeleteEntry(id); + await api.DeleteEntry(id); + OnDataChanged(); } [JSInvokable] - public Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) + public async Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) { - return api.CreateComplexFormComponent(complexFormComponent); + var createdComplexFormComponent = await api.CreateComplexFormComponent(complexFormComponent); + OnDataChanged(); + return createdComplexFormComponent; } [JSInvokable] - public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) + public async Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) { - return api.DeleteComplexFormComponent(complexFormComponent); + await api.DeleteComplexFormComponent(complexFormComponent); + OnDataChanged(); } [JSInvokable] - public Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) + public async Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) { - return api.AddComplexFormType(entryId, complexFormTypeId); + await api.AddComplexFormType(entryId, complexFormTypeId); + OnDataChanged(); } [JSInvokable] - public Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) + public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) { - return api.RemoveComplexFormType(entryId, complexFormTypeId); + await api.RemoveComplexFormType(entryId, complexFormTypeId); + OnDataChanged(); } [JSInvokable] - public Task CreateSense(Guid entryId, Sense sense) + public async Task CreateSense(Guid entryId, Sense sense) { - return api.CreateSense(entryId, sense); + var createdSense = await api.CreateSense(entryId, sense); + OnDataChanged(); + return createdSense; } [JSInvokable] - public Task UpdateSense(Guid entryId, Sense before, Sense after) + public async Task UpdateSense(Guid entryId, Sense before, Sense after) { - return api.UpdateSense(entryId, before, after); + var updatedSense = await api.UpdateSense(entryId, before, after); + OnDataChanged(); + return updatedSense; } [JSInvokable] - public Task DeleteSense(Guid entryId, Guid senseId) + public async Task DeleteSense(Guid entryId, Guid senseId) { - return api.DeleteSense(entryId, senseId); + await api.DeleteSense(entryId, senseId); + OnDataChanged(); } [JSInvokable] - public Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain) + public async Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain) { - return api.AddSemanticDomainToSense(senseId, semanticDomain); + await api.AddSemanticDomainToSense(senseId, semanticDomain); + OnDataChanged(); } [JSInvokable] - public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) + public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) { - return api.RemoveSemanticDomainFromSense(senseId, semanticDomainId); + await api.RemoveSemanticDomainFromSense(senseId, semanticDomainId); + OnDataChanged(); } [JSInvokable] - public Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence) + public async Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence) { - return api.CreateExampleSentence(entryId, senseId, exampleSentence); + var createdExampleSentence = await api.CreateExampleSentence(entryId, senseId, exampleSentence); + OnDataChanged(); + return createdExampleSentence; } [JSInvokable] - public Task UpdateExampleSentence(Guid entryId, Guid senseId, ExampleSentence before, ExampleSentence after) + public async Task UpdateExampleSentence(Guid entryId, Guid senseId, ExampleSentence before, ExampleSentence after) { - return api.UpdateExampleSentence(entryId, senseId, before, after); + var updatedExampleSentence = await api.UpdateExampleSentence(entryId, senseId, before, after); + OnDataChanged(); + return updatedExampleSentence; } [JSInvokable] - public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) + public async Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) { - return api.DeleteExampleSentence(entryId, senseId, exampleSentenceId); + await api.DeleteExampleSentence(entryId, senseId, exampleSentenceId); + OnDataChanged(); } public void Dispose() diff --git a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs index f7962ebda..dde5c94c4 100644 --- a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs +++ b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs @@ -3,6 +3,7 @@ using FwLiteShared.Projects; using FwLiteShared.Services; using LcmCrdt; +using Microsoft.JSInterop; using MiniLcm; using MiniLcm.Models; using Reinforced.Typings; @@ -31,12 +32,19 @@ public static void Configure(ConfigurationBuilder builder) builder.Substitute(typeof(Uri), new RtSimpleTypeName("string")); builder.Substitute(typeof(DateTimeOffset), new RtSimpleTypeName("string")); builder.SubstituteGeneric(typeof(ValueTask<>), (type, resolver) => resolver.ResolveTypeName(typeof(Task<>).MakeGenericType(type.GenericTypeArguments[0]), true)); + var dotnetObjectRefInterface = typeof(DotNetObjectReference<>).GetInterfaces().First(); + builder.SubstituteGeneric(typeof(DotNetObjectReference<>), (type, resolver) => resolver.ResolveTypeName(dotnetObjectRefInterface)); //todo generate a multistring type rather than just substituting it everywhere builder.ExportAsThirdParty().WithName("IMultiString").Imports([new () { From = "$lib/dotnet-types/i-multi-string", Target = "type {IMultiString}" }]); + builder.ExportAsThirdParty([dotnetObjectRefInterface], exportBuilder => exportBuilder.WithName("DotNet.DotNetObject").Imports([new () + { + From = "@microsoft/dotnet-js-interop", + Target = "type {DotNet}" + }])); builder.ExportAsInterface().WithPublicNonStaticProperties(exportBuilder => { if (exportBuilder.Member.Name == nameof(Sense.Order)) @@ -80,6 +88,7 @@ public static void Configure(ConfigurationBuilder builder) builder.ExportAsInterface().WithPublicProperties(); builder.ExportAsEnum().UseString(); builder.ExportAsEnum(); + builder.ExportAsInterface().WithPublicMethods(b => b.AlwaysReturnPromise()); } private static void AlwaysReturnPromise(this MethodExportBuilder exportBuilder) diff --git a/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs b/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs index 1d73fbf24..da0bc6621 100644 --- a/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs +++ b/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs @@ -33,7 +33,7 @@ public static WebApplication SetupAppServer(WebApplicationOptions options, Actio builder.ConfigureDev(config => config.LexboxServers = [ new (new("https://lexbox.dev.languagetechnology.org"), "Lexbox Dev"), - new (new("https://localhost:3000"), "Lexbox Local"), + new (new("https://localhost:3050"), "Lexbox Local"), new (new("https://staging.languagedepot.org"), "Lexbox Staging") ]); builder.ConfigureProd(config => diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index c9e74442b..1dfe7c287 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -71,6 +71,14 @@ Task UpdateExampleSentence(Guid entryId, #endregion } +/// +/// API for saving the project, really only used by FwData +/// +public interface IMiniLcmSaveApi +{ + void Save(); +} + /// /// wrapper around JsonPatchDocument that allows for fluent updates /// diff --git a/backend/FwLite/Taskfile.yml b/backend/FwLite/Taskfile.yml index aafd0df2c..779c751a7 100644 --- a/backend/FwLite/Taskfile.yml +++ b/backend/FwLite/Taskfile.yml @@ -7,20 +7,16 @@ includes: tasks: - web-for-develop: - label: dotnet - dir: ./FwLiteWeb - cmd: dotnet watch --no-hot-reload web: - label: Run FwLiteWeb with Local LexBox - env: - Auth__DefaultAuthority: "https://localhost:3050" + aliases: + - web-for-develop + label: Run FwLiteWeb with Local LexBox, requires vite dev server to be running, use task fw-lite-web in root dir: ./FwLiteWeb cmd: dotnet watch --no-hot-reload + maui-windows: - deps: [ ui:build-viewer-app ] - label: Run Maui Windows + label: Run Maui Windows, requires vite dev server to be running, use task fw-lite-win in root dir: ./FwLiteMaui cmd: dotnet run -f net9.0-windows10.0.19041.0 diff --git a/frontend/viewer/src/DotnetProjectView.svelte b/frontend/viewer/src/DotnetProjectView.svelte index 78ebb77c0..c0a488f30 100644 --- a/frontend/viewer/src/DotnetProjectView.svelte +++ b/frontend/viewer/src/DotnetProjectView.svelte @@ -1,22 +1,24 @@  {#if serviceLoaded} diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts index 14d613cae..0f7d83a62 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts @@ -5,6 +5,7 @@ export enum DotnetService { MiniLcmApi = "MiniLcmApi", + MiniLcmApiProvider = "MiniLcmApiProvider", CombinedProjectsService = "CombinedProjectsService", AuthService = "AuthService", ImportFwdataService = "ImportFwdataService", diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IMiniLcmApiProvider.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IMiniLcmApiProvider.ts new file mode 100644 index 000000000..ef3ae090c --- /dev/null +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IMiniLcmApiProvider.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// This code was generated by a Reinforced.Typings tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. + +import type {DotNet} from '@microsoft/dotnet-js-interop'; + +export interface IMiniLcmApiProvider +{ + getMiniLcmApi() : Promise; + setMiniLcmApi(miniLcmApi: DotNet.DotNetObject) : Promise; + clearMiniLcmApi() : Promise; +} +/* eslint-enable */ diff --git a/frontend/viewer/src/lib/services/service-provider-dotnet.ts b/frontend/viewer/src/lib/services/service-provider-dotnet.ts index 7bdafcf89..a7e389168 100644 --- a/frontend/viewer/src/lib/services/service-provider-dotnet.ts +++ b/frontend/viewer/src/lib/services/service-provider-dotnet.ts @@ -9,7 +9,7 @@ export class DotNetServiceProvider { this.services = globalThis.window.lexbox.FwLiteProvider ?? ({} as LexboxServiceRegistry); } - public async setOverrideServices(fwLiteProvider: LexboxServiceRegistry) { + public setOverrideServices(fwLiteProvider: LexboxServiceRegistry) { this.services = fwLiteProvider; } @@ -17,6 +17,10 @@ export class DotNetServiceProvider { return !!this.services[key]; } + public removeService(key: ServiceKey): void { + delete this.services[key]; + } + public getService(key: K): LexboxServiceRegistry[K] | undefined { this.validateAllServices(); const service = this.services[key] as LexboxServiceRegistry[K] | DotNet.DotNetObject | undefined; @@ -41,7 +45,7 @@ export class DotNetServiceProvider { } } -function wrapInProxy(dotnetObject: DotNet.DotNetObject): unknown { +export function wrapInProxy(dotnetObject: DotNet.DotNetObject): unknown { return new Proxy(dotnetObject, { get(target: DotNet.DotNetObject, prop: string) { const dotnetMethodName = uppercaseFirstLetter(prop); diff --git a/frontend/viewer/src/lib/services/service-provider.ts b/frontend/viewer/src/lib/services/service-provider.ts index d0deedec4..ca5737c70 100644 --- a/frontend/viewer/src/lib/services/service-provider.ts +++ b/frontend/viewer/src/lib/services/service-provider.ts @@ -5,6 +5,7 @@ import type {IImportFwdataService} from '$lib/dotnet-types/generated-types/FwLit import type {IMiniLcmJsInvokable} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IMiniLcmJsInvokable'; import {useEventBus} from './event-bus'; import type {IFwLiteConfig} from '$lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig'; +import type {IMiniLcmApiProvider} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IMiniLcmApiProvider'; export enum LexboxService { LexboxApi = 'LexboxApi' @@ -12,6 +13,7 @@ export enum LexboxService { export type ServiceKey = keyof LexboxServiceRegistry; export type LexboxServiceRegistry = { [DotnetService.MiniLcmApi]: IMiniLcmJsInvokable, + [DotnetService.MiniLcmApiProvider]: IMiniLcmApiProvider, [DotnetService.CombinedProjectsService]: ICombinedProjectsService, [DotnetService.AuthService]: IAuthService, [DotnetService.ImportFwdataService]: IImportFwdataService, @@ -28,6 +30,11 @@ export class LexboxServiceProvider { this.services[key] = service; } + public removeService(key: K): void { + this.validateServiceKey(key); + delete this.services[key]; + } + public getService(key: K): LexboxServiceRegistry[K] { this.validateServiceKey(key); const service = globalThis.window.lexbox.DotNetServiceProvider?.getService(key) ?? this.services[key]; @@ -69,3 +76,7 @@ export function useImportFwdataService(): IImportFwdataService { export function useFwLiteConfig(): IFwLiteConfig { return window.lexbox.ServiceProvider.getService(DotnetService.FwLiteConfig); } + +export function useMiniLcmApiProvider(): IMiniLcmApiProvider { + return window.lexbox.ServiceProvider.getService(DotnetService.MiniLcmApiProvider); +}