From a721446fec84edc412651c3b29c58a95d7794399 Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Thu, 29 Jan 2026 08:21:01 +1300 Subject: [PATCH 1/2] SF-3686 Enable SignalR reconnection with state keeping --- .../ClientApp/src/app/core/project-notification.service.ts | 6 +++++- src/SIL.XForge.Scripture/Startup.cs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts index 42fb0e51c39..87d15f41f6d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts @@ -16,7 +16,11 @@ export class ProjectNotificationService { private authService: AuthService, private readonly onlineService: OnlineStatusService ) { - this.connection = new HubConnectionBuilder().withUrl('/project-notifications', this.options).build(); + this.connection = new HubConnectionBuilder() + .withUrl('/project-notifications', this.options) + .withAutomaticReconnect() + .withStatefulReconnect() + .build(); } get appOnline(): boolean { diff --git a/src/SIL.XForge.Scripture/Startup.cs b/src/SIL.XForge.Scripture/Startup.cs index 5dcd4326567..d27600f0232 100644 --- a/src/SIL.XForge.Scripture/Startup.cs +++ b/src/SIL.XForge.Scripture/Startup.cs @@ -289,7 +289,10 @@ IExceptionHandler exceptionHandler { endpoints.MapControllers(); endpoints.MapRazorPages(); - endpoints.MapHub(pattern: $"/{UrlConstants.ProjectNotifications}"); + endpoints.MapHub( + pattern: $"/{UrlConstants.ProjectNotifications}", + options => options.AllowStatefulReconnects = true + ); var authOptions = Configuration.GetOptions(); endpoints.MapHangfireDashboard( new DashboardOptions From 4a3f1160d20e5f12a42e8adb78f240cf8c133d65 Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Thu, 29 Jan 2026 08:23:07 +1300 Subject: [PATCH 2/2] SF-3686 Release SignalR notification handlers --- .../src/app/core/project-notification.service.ts | 8 ++++++++ .../src/app/sync/sync-progress/sync-progress.component.ts | 8 +++++--- .../draft-history-list/draft-history-list.component.ts | 8 +++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts index 87d15f41f6d..84a72b91eb6 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts @@ -27,6 +27,14 @@ export class ProjectNotificationService { return this.onlineService.isOnline && this.onlineService.isBrowserOnline; } + removeNotifyBuildProgressHandler(handler: any): void { + this.connection.off('notifyBuildProgress', handler); + } + + removeNotifySyncProgressHandler(handler: any): void { + this.connection.off('notifySyncProgress', handler); + } + setNotifyBuildProgressHandler(handler: any): void { this.connection.on('notifyBuildProgress', handler); } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/sync/sync-progress/sync-progress.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/sync/sync-progress/sync-progress.component.ts index fa2160e7920..c5c6b791cd2 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/sync/sync-progress/sync-progress.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/sync/sync-progress/sync-progress.component.ts @@ -65,6 +65,9 @@ export class SyncProgressComponent { private sourceProjectDoc?: SFProjectDoc; private _projectDoc?: SFProjectDoc; + private readonly syncProgressHandler = (projectId: string, progressState: ProgressState): void => + this.updateProgressState(projectId, progressState); + constructor( private readonly projectService: SFProjectService, private readonly projectNotificationService: ProjectNotificationService, @@ -73,11 +76,10 @@ export class SyncProgressComponent { private readonly onlineStatus: OnlineStatusService, private destroyRef: DestroyRef ) { - this.projectNotificationService.setNotifySyncProgressHandler((projectId: string, progressState: ProgressState) => { - this.updateProgressState(projectId, progressState); - }); + this.projectNotificationService.setNotifySyncProgressHandler(this.syncProgressHandler); this.destroyRef.onDestroy(async () => { await this.projectNotificationService.stop(); + this.projectNotificationService.removeNotifySyncProgressHandler(this.syncProgressHandler); }); } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts index dc39d52e7ef..863f49de420 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts @@ -25,6 +25,9 @@ export class DraftHistoryListComponent { // This is just after SFv5.33.0 was released readonly draftHistoryCutOffDate: Date = new Date('2025-06-03T21:00:00Z'); readonly draftHistoryCutOffDateFormatted: string = this.i18n.formatDate(this.draftHistoryCutOffDate); + private readonly notifyBuildProgressHandler = (projectId: string): void => { + this.loadHistory(projectId); + }; constructor( activatedProject: ActivatedProjectService, @@ -48,13 +51,12 @@ export class DraftHistoryListComponent { await projectNotificationService.subscribeToProject(projectId); // When build notifications are received, reload the build history // NOTE: We do not need the build state, so just ignore it. - projectNotificationService.setNotifyBuildProgressHandler((projectId: string) => { - this.loadHistory(projectId); - }); + projectNotificationService.setNotifyBuildProgressHandler(this.notifyBuildProgressHandler); }); destroyRef.onDestroy(async () => { // Stop the SignalR connection when the component is destroyed await projectNotificationService.stop(); + projectNotificationService.removeNotifyBuildProgressHandler(this.notifyBuildProgressHandler); }); }