diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml index fa1e18081..65a875609 100644 --- a/.github/workflows/fw-lite.yaml +++ b/.github/workflows/fw-lite.yaml @@ -41,8 +41,7 @@ jobs: dotnet build --configuration Release - name: Dotnet test - working-directory: backend/FwLite/LcmCrdt.Tests - run: dotnet test --configuration Release --logger GitHubActions + run: dotnet test FwLiteOnly.slnf --configuration Release --logger GitHubActions - name: Build viewer working-directory: frontend/viewer diff --git a/FwLiteOnly.slnf b/FwLiteOnly.slnf new file mode 100644 index 000000000..c2dc53742 --- /dev/null +++ b/FwLiteOnly.slnf @@ -0,0 +1,18 @@ +{ + "solution": { + "path": "LexBox.sln", + "projects": [ + "backend\\FwLite\\MiniLcm\\MiniLcm.csproj", + "backend\\harmony\\src\\SIL.Harmony.Core\\SIL.Harmony.Core.csproj", + "backend\\harmony\\src\\SIL.Harmony\\SIL.Harmony.csproj", + "backend\\FwLite\\LcmCrdt\\LcmCrdt.csproj", + "backend\\FwLite\\LcmCrdt.Tests\\LcmCrdt.Tests.csproj", + "backend\\FWLite\\LocalWebApp\\LocalWebApp.csproj", + "backend\\FwLite\\FwLiteDesktop\\FwLiteDesktop.csproj", + "backend\\FWLite\\FwDataMiniLcmBridge\\FwDataMiniLcmBridge.csproj", + "backend\\FwLite\\FwDataMiniLcmBridge.Tests\\FwDataMiniLcmBridge.Tests.csproj", + "backend\\FWLite\\FwLiteProjectSync\\FwLiteProjectSync.csproj", + "backend\\FWLite\\FwLiteProjectSync.Tests\\FwLiteProjectSync.Tests.csproj", + ] + } +} diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj b/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj index 0503b4cdc..b1674de09 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj @@ -11,11 +11,18 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/PartOfSpeechTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/PartOfSpeechTests.cs index 73927166b..7ff6370dc 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/PartOfSpeechTests.cs +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/PartOfSpeechTests.cs @@ -16,22 +16,24 @@ public async Task InitializeAsync() _api = fixture.CreateApi(projectName); _api.Should().NotBeNull(); - var partOfSpeech = new PartOfSpeech() + var nounPos = new PartOfSpeech() { - Id = Guid.NewGuid(), Name = { { "en", "new-part-of-speech" } } + Id = Guid.NewGuid(), Name = { { "en", "Noun" } } }; - await _api.CreatePartOfSpeech(partOfSpeech); + await _api.CreatePartOfSpeech(nounPos); + + await _api.CreatePartOfSpeech(new() { Id = Guid.NewGuid(), Name = { { "en", "Verb" } } }); await _api.CreateEntry(new Entry() { Id = Guid.NewGuid(), - LexemeForm = {{"en", "new-lexeme-form"}}, + LexemeForm = {{"en", "Apple"}}, Senses = new List() { new Sense() { - Gloss = {{"en", "new-sense-gloss"}}, - PartOfSpeechId = partOfSpeech.Id + Gloss = {{"en", "Fruit"}}, + PartOfSpeechId = nounPos.Id } }}); } @@ -45,7 +47,10 @@ public async Task DisposeAsync() public async Task GetPartsOfSpeech_ReturnsAllPartsOfSpeech() { var partOfSpeeches = await _api.GetPartsOfSpeech().ToArrayAsync(); - partOfSpeeches.Should().AllSatisfy(po => po.Id.Should().NotBe(Guid.Empty)); + partOfSpeeches.Should() + .NotBeEmpty() + .And + .AllSatisfy(po => po.Id.Should().NotBe(Guid.Empty)); } [Fact] diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 1f368c8a2..22f0529af 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -415,11 +415,26 @@ public Task DeleteEntry(Guid id) internal void CreateSense(ILexEntry lexEntry, Sense sense) { var lexSense = LexSenseFactory.Create(sense.Id, lexEntry); + var msa = new SandboxGenericMSA() { MsaType = lexSense.GetDesiredMsaType() }; + if (sense.PartOfSpeechId.HasValue && PartOfSpeechRepository.TryGetObject(sense.PartOfSpeechId.Value, out var pos)) + { + msa.MainPOS = pos; + } + lexSense.SandboxMSA = msa; ApplySenseToLexSense(sense, lexSense); } private void ApplySenseToLexSense(Sense sense, ILexSense lexSense) { + if (lexSense.MorphoSyntaxAnalysisRA.GetPartOfSpeech()?.Guid != sense.PartOfSpeechId) + { + IPartOfSpeech? pos = null; + if (sense.PartOfSpeechId.HasValue) + { + pos = PartOfSpeechRepository.GetObject(sense.PartOfSpeechId.Value); + } + lexSense.MorphoSyntaxAnalysisRA.SetMsaPartOfSpeech(pos); + } UpdateLcmMultiString(lexSense.Gloss, sense.Gloss); UpdateLcmMultiString(lexSense.Definition, sense.Definition); foreach (var senseSemanticDomain in sense.SemanticDomains) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/MorphoSyntaxExtensions.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/MorphoSyntaxExtensions.cs index 6480fcd3e..25eee42ad 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/MorphoSyntaxExtensions.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/MorphoSyntaxExtensions.cs @@ -6,6 +6,8 @@ public static class MorphoSyntaxExtensions { public static void SetMsaPartOfSpeech(this IMoMorphSynAnalysis msa, IPartOfSpeech? pos) { + ArgumentNullException.ThrowIfNull(msa); + switch (msa.ClassID) { case MoDerivAffMsaTags.kClassId: @@ -31,6 +33,7 @@ public static void SetMsaPartOfSpeech(this IMoMorphSynAnalysis msa, IPartOfSpeec public static IPartOfSpeech? GetPartOfSpeech(this IMoMorphSynAnalysis msa) { + ArgumentNullException.ThrowIfNull(msa); switch (msa.ClassID) { case MoDerivAffMsaTags.kClassId: diff --git a/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj b/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj index c26a852d8..84d386c43 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj +++ b/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj @@ -11,12 +11,19 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj b/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj index 17aa92226..b9e9c0924 100644 --- a/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj +++ b/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj @@ -12,11 +12,14 @@ - + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/backend/FwLite/LocalWebApp/BackgroundSyncService.cs b/backend/FwLite/LocalWebApp/BackgroundSyncService.cs index cc14a168a..bbe93c26c 100644 --- a/backend/FwLite/LocalWebApp/BackgroundSyncService.cs +++ b/backend/FwLite/LocalWebApp/BackgroundSyncService.cs @@ -74,9 +74,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async Task SyncProject(CrdtProject crdtProject) { - await using var serviceScope = projectsService.CreateProjectScope(crdtProject); - await serviceScope.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); - var syncService = serviceScope.ServiceProvider.GetRequiredService(); - return await syncService.ExecuteSync(); + try + { + await using var serviceScope = projectsService.CreateProjectScope(crdtProject); + await serviceScope.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); + var syncService = serviceScope.ServiceProvider.GetRequiredService(); + return await syncService.ExecuteSync(); + } + catch (Exception e) + { + logger.LogError(e, "Error syncing project {ProjectId}", crdtProject.Name); + return new SyncResults([], [], false); + } } } diff --git a/backend/Testing/Testing.csproj b/backend/Testing/Testing.csproj index b416bb438..e2e6e66b7 100644 --- a/backend/Testing/Testing.csproj +++ b/backend/Testing/Testing.csproj @@ -31,7 +31,7 @@ - + @@ -39,8 +39,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/frontend/https-proxy/package.json b/frontend/https-proxy/package.json index 2e333b1be..ab08648e5 100644 --- a/frontend/https-proxy/package.json +++ b/frontend/https-proxy/package.json @@ -2,7 +2,11 @@ "name": "https-proxy", "version": "0.0.1", "private": true, - "packageManager": "pnpm@8.15.1", + "packageManager": "pnpm@9.1.2", + "engines": { + "node": ">=20", + "pnpm": ">=9" + }, "type": "module", "scripts": { "dev": "vite" diff --git a/frontend/viewer/package.json b/frontend/viewer/package.json index 7ae3f6c34..326e7b36b 100644 --- a/frontend/viewer/package.json +++ b/frontend/viewer/package.json @@ -1,9 +1,10 @@ { "name": "viewer", "private": true, - "packageManager": "pnpm@8.15.1", + "packageManager": "pnpm@9.1.2", "engines": { - "node": ">=20" + "node": ">=20", + "pnpm": ">=9" }, "version": "1.0.0", "type": "module", diff --git a/frontend/viewer/src/CrdtProjectView.svelte b/frontend/viewer/src/CrdtProjectView.svelte index 05c9164bf..d51e56703 100644 --- a/frontend/viewer/src/CrdtProjectView.svelte +++ b/frontend/viewer/src/CrdtProjectView.svelte @@ -1,24 +1,12 @@  - + diff --git a/frontend/viewer/src/FwDataProjectView.svelte b/frontend/viewer/src/FwDataProjectView.svelte index 2bb42cbfe..9094b2368 100644 --- a/frontend/viewer/src/FwDataProjectView.svelte +++ b/frontend/viewer/src/FwDataProjectView.svelte @@ -1,50 +1,25 @@  - + diff --git a/frontend/viewer/src/lib/services/service-provider-signalr.ts b/frontend/viewer/src/lib/services/service-provider-signalr.ts index 656b3d7de..afaed4057 100644 --- a/frontend/viewer/src/lib/services/service-provider-signalr.ts +++ b/frontend/viewer/src/lib/services/service-provider-signalr.ts @@ -1,8 +1,11 @@ import {getHubProxyFactory, getReceiverRegister} from '../generated-signalr-client/TypedSignalR.Client'; -import type { HubConnection } from '@microsoft/signalr'; +import {type HubConnection, HubConnectionBuilder, HubConnectionState} from '@microsoft/signalr'; import type { LexboxApiFeatures, LexboxApiMetadata } from './lexbox-api'; import {LexboxService} from './service-provider'; +import {onDestroy} from 'svelte'; +import {type Writable, writable} from 'svelte/store'; +import {AppNotification} from '../notifications/notifications'; import { CloseReason, type ILexboxClient @@ -10,27 +13,24 @@ import { import {useEventBus} from './event-bus'; import {Entry} from '../mini-lcm'; -type ErrorContext = {error: Error|unknown, methodName: string}; -export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatures, onError?: (context: ErrorContext) => void) { +type ErrorContext = {error: Error|unknown, methodName?: string, origin: 'method'|'connection'}; +type ErrorHandler = (errorContext: ErrorContext) => {handled: boolean}; +export function SetupSignalR(url: string, + features: LexboxApiFeatures, + onError?: ErrorHandler) { + let {connection, connected} = setupConnection(url, errorContext => { + if (onError && onError(errorContext).handled) { + return {handled: true}; + } + if (errorContext.error instanceof Error) { + let message = errorContext.error.message; + AppNotification.display('Connection error: ' + message, 'error', 'long'); + } else { + AppNotification.display('Unknown Connection error', 'error', 'long'); + } + return {handled: true}; + }); const hubFactory = getHubProxyFactory('ILexboxApiHub'); - if (onError) { - connection = new Proxy(connection, { - get(target, prop: keyof HubConnection, receiver) { - if (prop === 'invoke') { - return async (methodName: string, ...args: any[]) => { - try { - return await target.invoke(methodName, ...args); - } catch (e) { - onError({error: e, methodName}); - throw e; - } - } - } else { - return Reflect.get(target, prop, receiver); - } - } - }) as HubConnection; - } const hubProxy = hubFactory.createHubProxy(connection); const lexboxApiHubProxy = Object.assign(hubProxy, { @@ -38,7 +38,6 @@ export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatu return features; } } satisfies LexboxApiMetadata); - const changeEventBus = useEventBus(); getReceiverRegister('ILexboxClient').register(connection, { OnProjectClosed(reason: CloseReason): Promise { @@ -51,4 +50,43 @@ export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatu } }); window.lexbox.ServiceProvider.setService(LexboxService.LexboxApi, lexboxApiHubProxy); + return {connected, lexboxApi: lexboxApiHubProxy}; +} + +function setupConnection(url: string, onError: ErrorHandler): {connection: HubConnection, connected: Writable} { + const connected = writable(false) + let connection = new HubConnectionBuilder() + .withUrl(url) + .withAutomaticReconnect() + .build(); + onDestroy(() => connection.stop()); + connection.onclose((error) => { + connected.set(false); + if (!error) return; + console.error('Connection closed', error); + onError({error, origin: 'connection'}); + }); + void connection.start() + .then(() => connected.set(connection.state == HubConnectionState.Connected)) + .catch(err => { + onError({error: err, origin: 'connection'}); + console.error('error connecting to signalr', err); + }); + connection = new Proxy(connection, { + get(target, prop: keyof HubConnection, receiver) { + if (prop === 'invoke') { + return async (methodName: string, ...args: any[]) => { + try { + return await target.invoke(methodName, ...args); + } catch (e) { + onError({error: e, methodName, origin: 'method'}); + throw e; + } + } + } else { + return Reflect.get(target, prop, receiver); + } + } + }) as HubConnection; + return {connection, connected}; }