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};
}