diff --git a/packages/api/src/models/broadcast/broadcast-message.ts b/packages/api/src/models/broadcast/broadcast-message.ts new file mode 100644 index 0000000000..0106994989 --- /dev/null +++ b/packages/api/src/models/broadcast/broadcast-message.ts @@ -0,0 +1,16 @@ +import {MessageType} from './message-type'; + +/** + * BroadcastMessage represents a message that can be broadcasted to all browser contexts. + * No methods should be added here, since the broadcast API uses structured cloning to transfer messages, + * which removes any methods from the message. + */ +export class BroadcastMessage { + readonly type: MessageType; + readonly params?: unknown; + + constructor(type: MessageType, params?: unknown) { + this.type = type; + this.params = params; + } +} diff --git a/packages/api/src/models/broadcast/index.ts b/packages/api/src/models/broadcast/index.ts new file mode 100644 index 0000000000..eb497640ac --- /dev/null +++ b/packages/api/src/models/broadcast/index.ts @@ -0,0 +1,2 @@ +export {MessageType} from './message-type'; +export {BroadcastMessage} from './broadcast-message'; diff --git a/packages/api/src/models/broadcast/message-type.ts b/packages/api/src/models/broadcast/message-type.ts new file mode 100644 index 0000000000..a5349060f2 --- /dev/null +++ b/packages/api/src/models/broadcast/message-type.ts @@ -0,0 +1,3 @@ +export enum MessageType { + REPOSITORIES_UPDATED = 'repositoriesUpdated', +} diff --git a/packages/api/src/ontotext-workbench-api.ts b/packages/api/src/ontotext-workbench-api.ts index 4ddbd019f2..13f6d179d3 100644 --- a/packages/api/src/ontotext-workbench-api.ts +++ b/packages/api/src/ontotext-workbench-api.ts @@ -20,6 +20,7 @@ export * from './models/plugins'; export * from './models/configuration'; export * from './models/notification'; export * from './models/translation'; +export * from './models/broadcast'; // Export providers for external usages. export * from './providers'; @@ -48,6 +49,7 @@ export * from './services/plugins'; export * from './services/configuration'; export * from './services/logging'; export * from './services/notification'; +export * from './services/broadcast'; // Export utils for external usages. export * from './services/utils'; diff --git a/packages/api/src/services/broadcast/broadcast.service.ts b/packages/api/src/services/broadcast/broadcast.service.ts new file mode 100644 index 0000000000..21458f767a --- /dev/null +++ b/packages/api/src/services/broadcast/broadcast.service.ts @@ -0,0 +1,43 @@ +import {BroadcastMessage} from '../../models/broadcast'; + +type MessageHandler = (message: BroadcastMessage) => void; + +/** + * A service that facilitates communication between different browser contexts + * (such as tabs, windows, or iframes) of the same origin. + * It uses the `BroadcastChannel` API to send and receive messages. + */ +export class BroadcastService { + private readonly CHANNEL_NAME = 'ontotext-workbench-broadcast'; + private readonly MESSAGE_EVENT = 'message'; + + private channel?: BroadcastChannel; + + /** + * Sends a message to all listening browser contexts on the same origin. + * @param {BroadcastMessage} message The message to be sent. + */ + sendMessage(message: BroadcastMessage): void { + this.getChannel().postMessage(message); + } + + /** + * Subscribes to incoming messages. + * @param {MessageHandler} callback The function to be executed when a message is received. + * @returns {() => void} An unsubscribe function. + */ + onMessageReceived(callback: MessageHandler): () => void { + const channel = this.getChannel(); + const listener = (event: MessageEvent) => callback(event.data); + + channel.addEventListener(this.MESSAGE_EVENT, listener); + return () => channel.removeEventListener(this.MESSAGE_EVENT, listener); + } + + private getChannel(): BroadcastChannel { + if (!this.channel) { + this.channel = new BroadcastChannel(this.CHANNEL_NAME); + } + return this.channel; + } +} diff --git a/packages/api/src/services/broadcast/index.ts b/packages/api/src/services/broadcast/index.ts new file mode 100644 index 0000000000..11a4a56c57 --- /dev/null +++ b/packages/api/src/services/broadcast/index.ts @@ -0,0 +1 @@ +export {BroadcastService} from './broadcast.service'; diff --git a/packages/legacy-workbench/src/js/angular/controllers.js b/packages/legacy-workbench/src/js/angular/controllers.js index 4ab4795ab1..536cbf2760 100644 --- a/packages/legacy-workbench/src/js/angular/controllers.js +++ b/packages/legacy-workbench/src/js/angular/controllers.js @@ -37,8 +37,10 @@ import { EventService, RepositoryContextService, RepositoryService, - ServiceProvider, + service, SecurityContextService, + BroadcastService, + MessageType, } from "@ontotext/workbench-api"; import {EventConstants} from "./utils/event-constants"; import {CookieConsent} from "./models/cookie-policy/cookie-consent"; @@ -110,14 +112,14 @@ function homeCtrl($scope, // Public functions // ========================= $scope.getActiveRepositorySize = () => { - const repo = ServiceProvider.get(RepositoryContextService).getSelectedRepository(); + const repo = service(RepositoryContextService).getSelectedRepository(); if (!repo) { return; } $scope.activeRepositorySizeError = undefined; - ServiceProvider.get(RepositoryService).getRepositorySizeInfo(repo) + service(RepositoryService).getRepositorySizeInfo(repo) .then(function(repositorySizeInfo) { $scope.activeRepositorySize = repositorySizeInfo; }) @@ -138,7 +140,7 @@ function homeCtrl($scope, const subscriptions = []; const onSelectedRepositoryUpdated = (repository) => { - const authService = ServiceProvider.get(AuthenticationService); + const authService = service(AuthenticationService); const hasGqlRights = authService.hasGqlRights(repository); // Don't call API, if no repo ID, or with GQL read-only or write rights @@ -161,7 +163,7 @@ function homeCtrl($scope, $scope.isAutocompleteEnabled = autocompleteEnabled; }; - subscriptions.push(ServiceProvider.get(RepositoryContextService).onSelectedRepositoryChanged(onSelectedRepositoryUpdated)); + subscriptions.push(service(RepositoryContextService).onSelectedRepositoryChanged(onSelectedRepositoryUpdated)); subscriptions.push(WorkbenchContextService.onAutocompleteEnabledUpdated(onAutocompleteEnabledUpdated)); $scope.$on('$destroy', () => subscriptions.forEach((subscription) => subscription())); @@ -209,7 +211,7 @@ function mainCtrl($scope, $menuItems, $jwtAuth, $http, toastr, $location, $repos // Update page permissions based on menu item role const updatePermissions = () => { - const securityContextService = ServiceProvider.get(SecurityContextService); + const securityContextService = service(SecurityContextService); const restrictedPages = securityContextService.getRestrictedPages(); const isAdministratorUser = $jwtAuth.isAdmin(); @@ -616,9 +618,9 @@ function mainCtrl($scope, $menuItems, $jwtAuth, $http, toastr, $location, $repos }); } - ServiceProvider.get(EventService).subscribe(EventName.LOGOUT, () => logout()); + service(EventService).subscribe(EventName.LOGOUT, () => logout()); - ServiceProvider.get(EventService).subscribe(EventConstants.RDF_SEARCH_ICON_CLICKED, () => { + service(EventService).subscribe(EventConstants.RDF_SEARCH_ICON_CLICKED, () => { $rootScope.$broadcast('rdfResourceSearchExpanded'); }); @@ -658,7 +660,7 @@ function mainCtrl($scope, $menuItems, $jwtAuth, $http, toastr, $location, $repos const localStoreChangeHandler = (localStoreEvent) => { if (authEvents.includes(localStoreEvent.key)) { - const newAuthenticationState = ServiceProvider.get(AuthenticationStorageService).isAuthenticated(); + const newAuthenticationState = service(AuthenticationStorageService).isAuthenticated(); $jwtAuth.updateReturnUrl(); if (isAuthenticated !== newAuthenticationState) { isAuthenticated = newAuthenticationState; @@ -1150,11 +1152,11 @@ function mainCtrl($scope, $menuItems, $jwtAuth, $http, toastr, $location, $repos // properties. let onSelectedRepositoryChangedSubscription; // subscribe to repository changes, once we are certain they are loaded - const onAppDataLoaded = ServiceProvider.get(ApplicationLifecycleContextService).onApplicationDataStateChanged((dataLoaded) => { + const onAppDataLoaded = service(ApplicationLifecycleContextService).onApplicationDataStateChanged((dataLoaded) => { if (dataLoaded) { // unsubscribe of any previous subscription onSelectedRepositoryChangedSubscription?.(); - onSelectedRepositoryChangedSubscription = ServiceProvider.get(RepositoryContextService) + onSelectedRepositoryChangedSubscription = service(RepositoryContextService) .onSelectedRepositoryChanged((repository) => { $repositories.onRepositorySet(repository); }); @@ -1168,9 +1170,16 @@ function mainCtrl($scope, $menuItems, $jwtAuth, $http, toastr, $location, $repos }); }; + const broadcastUnsubscribe = service(BroadcastService).onMessageReceived((message) => { + if (message.type === MessageType.REPOSITORIES_UPDATED) { + $repositories.init(); + } + }); + $scope.$on('$destroy', () => { onSelectedRepositoryChangedSubscription?.(); cookieConsentChangedSubscription?.(); + broadcastUnsubscribe?.(); onAppDataLoaded(); document.removeEventListener('click', closeActiveRepoPopoverEventHandler); window.removeEventListener('storage', localStoreChangeHandler); diff --git a/packages/legacy-workbench/src/js/angular/core/services/repositories.service.js b/packages/legacy-workbench/src/js/angular/core/services/repositories.service.js index e76b88dc88..c8970ad81c 100644 --- a/packages/legacy-workbench/src/js/angular/core/services/repositories.service.js +++ b/packages/legacy-workbench/src/js/angular/core/services/repositories.service.js @@ -17,6 +17,9 @@ import { RepositoryLocationContextService, MapperProvider, RepositoryListMapper, + BroadcastService, + BroadcastMessage, + MessageType, } from "@ontotext/workbench-api"; const modules = [ @@ -429,6 +432,7 @@ repositories.service('$repositories', ['toastr', '$rootScope', '$timeout', '$loc return RepositoriesRestService.deleteRepository(repo) .success(function() { that.init(); + service(BroadcastService).sendMessage(new BroadcastMessage(MessageType.REPOSITORIES_UPDATED)); }).error(function(data) { const msg = getError(data); toastr.error(msg, $translate.instant('common.error')); diff --git a/packages/legacy-workbench/src/js/angular/repositories/controllers.js b/packages/legacy-workbench/src/js/angular/repositories/controllers.js index 055ea6bdbc..3434b2a94e 100644 --- a/packages/legacy-workbench/src/js/angular/repositories/controllers.js +++ b/packages/legacy-workbench/src/js/angular/repositories/controllers.js @@ -7,6 +7,7 @@ import { import {decodeHTML} from "../../../app"; import './controllers/manage-remote-location-dialog.controller'; import {RemoteLocationType} from "../models/repository/remote-location.model"; +import {service, BroadcastService, MessageType, BroadcastMessage} from '@ontotext/workbench-api'; const ENTITY_INDEX_SIZE_MIN = 10000; @@ -667,6 +668,7 @@ function AddRepositoryCtrl($rootScope, $scope, toastr, $repositories, $location, RepositoriesRestService.createRepository($scope.repositoryInfo) .then(function() { toastr.success($translate.instant('created.repo.success.msg', {repoId: $scope.repositoryInfo.id})); + service(BroadcastService).sendMessage(new BroadcastMessage(MessageType.REPOSITORIES_UPDATED)); return $repositories.init(); }) .then(() => $scope.goBackToPreviousLocation())