From c99d3a3c457c7dae32b27636c79a5f5acb6e8858 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sat, 14 Mar 2026 22:22:09 -0600 Subject: [PATCH 01/36] feat(websockets): migrate WebSocket stack to data-access and react to site CRUD events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move WebSocket service from deprecated dotcms-js to libs/data-access as DotEventsSocket using native WebSocket only (dropped long-polling — all modern browsers support WS) - Add withWebSocket() feature to GlobalStore managing connection lifecycle, wsStatus signal, portletLayoutUpdated and siteEvents observables - Replace DotcmsEventsService.subscribeTo('UPDATE_PORTLET_LAYOUTS') in DotNavigationService with globalStore.portletLayoutUpdated - React to site CRUD events (SAVE, PUBLISH, UN_PUBLISH, UPDATE, ARCHIVE, UN_ARCHIVE, DELETE) in DotSiteComponent: debounce list refresh, switch to default site when selected site becomes unavailable (archived, stopped, deleted) - Add UN_PUBLISH_SITE to SystemEventType so unpublishHost DWR fires the WebSocket event Co-Authored-By: Claude Sonnet 4.6 --- core-web/apps/dotcms-ui/src/app/providers.ts | 8 +- .../services/dot-navigation.service.ts | 39 +++-- core-web/libs/data-access/src/index.ts | 4 + .../dot-websocket/dot-event-message.model.ts | 4 + .../dot-websocket/dot-events-socket-url.ts | 22 +++ .../dot-events-socket.service.ts | 152 ++++++++++++++++++ core-web/libs/global-store/src/index.ts | 1 + .../with-websocket.feature.spec.ts | 81 ++++++++++ .../with-websocket/with-websocket.feature.ts | 77 +++++++++ .../libs/global-store/src/lib/store.spec.ts | 16 +- core-web/libs/global-store/src/lib/store.ts | 2 + .../components/dot-site/dot-site.component.ts | 67 +++++++- .../api/system/event/SystemEventType.java | 7 +- 13 files changed, 452 insertions(+), 28 deletions(-) create mode 100644 core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts create mode 100644 core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts create mode 100644 core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts create mode 100644 core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts create mode 100644 core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index d3929f03264f..b699b08ea147 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -5,6 +5,7 @@ import { ConfirmationService } from 'primeng/api'; import { CanDeactivateGuardService, + DOT_EVENTS_SOCKET_URL, DotAlertConfirmService, DotAppsService, DotContentletService, @@ -13,6 +14,8 @@ import { DotCrudService, DotCurrentUserService, DotEventsService, + DotEventsSocket, + DotEventsSocketURL, DotFormatDateService, DotGenerateSecurePasswordService, DotGlobalMessageService, @@ -39,8 +42,6 @@ import { CoreWebService, DotcmsConfigService, DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, LoginService, @@ -79,6 +80,7 @@ const dotEventSocketURLFactory = () => { ); }; + const PROVIDERS: Provider[] = [ { provide: LOCATION_TOKEN, useValue: window.location }, EmaAppConfigurationService, @@ -128,7 +130,7 @@ const PROVIDERS: Provider[] = [ DotcmsEventsService, LoggerService, LoginService, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, + { provide: DOT_EVENTS_SOCKET_URL, useFactory: dotEventSocketURLFactory }, DotEventsSocket, StringUtils, UserModel, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts index a43be04b3990..eae7088c4dea 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts @@ -7,7 +7,7 @@ import { Event, NavigationEnd, Router } from '@angular/router'; import { filter, map, switchMap, take, tap } from 'rxjs/operators'; import { DotIframeService, DotRouterService } from '@dotcms/data-access'; -import { Auth, DotcmsEventsService, LoginService } from '@dotcms/dotcms-js'; +import { Auth, LoginService } from '@dotcms/dotcms-js'; import { DotMenu } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; @@ -19,7 +19,6 @@ export class DotNavigationService { private dotIframeService = inject(DotIframeService); private dotMenuService = inject(DotMenuService); private dotRouterService = inject(DotRouterService); - private dotcmsEventsService = inject(DotcmsEventsService); private dynamicRouteService = inject(DynamicRouteService); private loginService = inject(LoginService); private router = inject(Router); @@ -79,24 +78,24 @@ export class DotNavigationService { ) .subscribe(); - // Handle portlet layout updates - this.dotcmsEventsService.subscribeTo('UPDATE_PORTLET_LAYOUTS').subscribe(() => { - this.dotMenuService - .reloadMenu() - .pipe(take(1)) - .subscribe((menus: DotMenu[]) => { - this.registerDynamicRoutes(menus); - this.#globalStore.loadMenu(menus); - - if (this.dotRouterService.currentPortlet.id) { - this.#globalStore.setActiveMenu({ - portletId: this.dotRouterService.currentPortlet.id, - shortParentMenuId: this.dotRouterService.queryParams['mId'], - breadcrumbs: this.#globalStore.breadcrumbs() - }); - } - }); - }); + // Handle portlet layout updates from the global store WebSocket feature + this.#globalStore + .portletLayoutUpdated$() + .pipe( + switchMap(() => this.dotMenuService.reloadMenu().pipe(take(1))) + ) + .subscribe((menus: DotMenu[]) => { + this.registerDynamicRoutes(menus); + this.#globalStore.loadMenu(menus); + + if (this.dotRouterService.currentPortlet.id) { + this.#globalStore.setActiveMenu({ + portletId: this.dotRouterService.currentPortlet.id, + shortParentMenuId: this.dotRouterService.queryParams['mId'], + breadcrumbs: this.#globalStore.breadcrumbs() + }); + } + }); // Handle login/auth changes this.loginService.auth$ diff --git a/core-web/libs/data-access/src/index.ts b/core-web/libs/data-access/src/index.ts index 44fd9d059055..3c21bb47a844 100644 --- a/core-web/libs/data-access/src/index.ts +++ b/core-web/libs/data-access/src/index.ts @@ -72,3 +72,7 @@ export * from './lib/dot-page-contenttype/dot-page-contenttype.service'; export * from './lib/dot-favorite-contenttype/dot-favorite-contenttype.service'; export * from './lib/dot-content-drive/dot-content-drive.service'; export * from './lib/dot-usage/dot-usage.service'; + +export * from './lib/dot-websocket/dot-events-socket.service'; +export * from './lib/dot-websocket/dot-events-socket-url'; +export * from './lib/dot-websocket/dot-event-message.model'; diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts new file mode 100644 index 000000000000..e048d80945fa --- /dev/null +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts @@ -0,0 +1,4 @@ +export interface DotEventMessage { + event: string; + payload: { data: unknown }; +} diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts new file mode 100644 index 000000000000..eee99a51bae2 --- /dev/null +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts @@ -0,0 +1,22 @@ +import { InjectionToken } from '@angular/core'; + +/** + * Represents the URL configuration for the WebSocket event endpoint. + * Provide this via DotEventsSocketURL injection token. + */ +export class DotEventsSocketURL { + constructor( + private url: string, + private useSSL: boolean + ) {} + + getWebSocketURL(): string { + return `${this.useSSL ? 'wss' : 'ws'}://${this.url}`; + } + + getLongPollingURL(): string { + return `${this.useSSL ? 'https' : 'http'}://${this.url}`; + } +} + +export const DOT_EVENTS_SOCKET_URL = new InjectionToken('DotEventsSocketURL'); diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts new file mode 100644 index 000000000000..ea40d5a83bf8 --- /dev/null +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts @@ -0,0 +1,152 @@ +import { BehaviorSubject, Observable, Subject, timer } from 'rxjs'; + +import { Injectable, inject } from '@angular/core'; + +import { distinctUntilChanged, filter, map } from 'rxjs/operators'; + +import { DotEventMessage } from './dot-event-message.model'; +import { DOT_EVENTS_SOCKET_URL } from './dot-events-socket-url'; + +export type WebSocketStatus = 'connecting' | 'reconnecting' | 'connected' | 'closed'; + +/** + * Manages the WebSocket connection to the dotCMS server-sent events endpoint. + * + * Features: + * - Native WebSocket (no long-polling fallback — all modern browsers support WS) + * - Exponential backoff with jitter on reconnection + * - `status$()` — reactive connection state for UI indicators + * - `on(eventType)` — typed event subscription + */ +@Injectable({ providedIn: 'root' }) +export class DotEventsSocket { + private readonly socketURL = inject(DOT_EVENTS_SOCKET_URL); + + private socket: WebSocket | null = null; + private status: WebSocketStatus = 'connecting'; + private retryCount = 0; + private destroyed = false; + + private readonly _message = new Subject(); + private readonly _status = new BehaviorSubject('connecting'); + + private readonly MAX_RETRIES = 100_000; + private readonly INITIAL_RETRY_DELAY = 1_000; + private readonly MAX_RETRY_DELAY = 30_000; + + /** + * Opens the WebSocket connection. Call once at app startup. + * Returns an Observable that completes immediately after initiating the connection — + * subscribe in GlobalStore's withWebSocket feature. + */ + connect(): Observable { + return new Observable((subscriber) => { + this.destroyed = false; + this.openSocket(); + subscriber.next(); + subscriber.complete(); + }); + } + + /** Closes the connection permanently (e.g. on logout). */ + destroy(): void { + this.destroyed = true; + this.setStatus('closed'); + this.socket?.close(); + this.socket = null; + } + + /** Emits the typed payload of a specific event type. */ + on(eventType: string): Observable { + return this._message.asObservable().pipe( + filter((msg) => msg.event === eventType), + map((msg) => msg.payload?.data as T) + ); + } + + /** All raw messages from the server. */ + messages(): Observable { + return this._message.asObservable(); + } + + isConnected(): boolean { + return this.status === 'connected'; + } + + /** Emits only when the status actually changes. */ + status$(): Observable { + return this._status.asObservable().pipe(distinctUntilChanged()); + } + + private openSocket(): void { + if (this.destroyed) { + return; + } + + this.setStatus(this.retryCount === 0 ? 'connecting' : 'reconnecting'); + + try { + this.socket = new WebSocket(this.socketURL.getWebSocketURL()); + } catch { + this.scheduleReconnect(); + return; + } + + this.socket.onopen = () => { + this.retryCount = 0; + this.setStatus('connected'); + }; + + this.socket.onmessage = (ev: MessageEvent) => { + try { + this._message.next(JSON.parse(ev.data) as DotEventMessage); + } catch { + // Ignore unparseable messages + } + }; + + this.socket.onclose = (ev: CloseEvent) => { + if (!this.destroyed) { + // 1000 = normal closure (server-initiated clean close) — still reconnect + // since dotCMS may restart + if (ev.code !== 1001) { + this.scheduleReconnect(); + } + } + }; + + this.socket.onerror = () => { + // onerror is always followed by onclose, so let onclose drive reconnection + }; + } + + private scheduleReconnect(): void { + if (this.retryCount >= this.MAX_RETRIES || this.destroyed) { + this.setStatus('closed'); + return; + } + + this.retryCount++; + this.setStatus('reconnecting'); + + timer(this.calculateDelay()).subscribe(() => { + if (!this.destroyed) { + this.openSocket(); + } + }); + } + + private calculateDelay(): number { + const exponential = Math.min( + this.INITIAL_RETRY_DELAY * Math.pow(2, Math.min(this.retryCount, 10)), + this.MAX_RETRY_DELAY + ); + + return exponential + Math.random() * 1_000; + } + + private setStatus(status: WebSocketStatus): void { + this.status = status; + this._status.next(status); + } +} diff --git a/core-web/libs/global-store/src/index.ts b/core-web/libs/global-store/src/index.ts index 9f445f8edd3c..fd334876c105 100644 --- a/core-web/libs/global-store/src/index.ts +++ b/core-web/libs/global-store/src/index.ts @@ -1 +1,2 @@ export * from './lib/store'; +export { WebSocketStatus } from '@dotcms/data-access'; diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts new file mode 100644 index 000000000000..1972ab287e38 --- /dev/null +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts @@ -0,0 +1,81 @@ +import { signalStore, withState } from '@ngrx/signals'; +import { Subject, of, throwError } from 'rxjs'; + +import { TestBed } from '@angular/core/testing'; + +import { DotEventsSocket } from '@dotcms/data-access'; + +import { withWebSocket } from './with-websocket.feature'; + +describe('withWebSocket Feature', () => { + const TestStore = signalStore(withState({}), withWebSocket()); + + let store: InstanceType; + let statusSubject: Subject<'connecting' | 'reconnecting' | 'connected' | 'closed'>; + let mockEventsSocket: jest.Mocked>; + + beforeEach(() => { + statusSubject = new Subject(); + + mockEventsSocket = { + connect: jest.fn().mockReturnValue(of({})), + status$: jest.fn().mockReturnValue(statusSubject.asObservable()), + on: jest.fn().mockReturnValue(new Subject()), + destroy: jest.fn() + }; + + TestBed.configureTestingModule({ + providers: [ + TestStore, + { provide: DotEventsSocket, useValue: mockEventsSocket } + ] + }); + + store = TestBed.inject(TestStore); + }); + + it('should initialize with connecting status', () => { + expect(store.wsStatus()).toBe('connecting'); + }); + + it('should call connect and trackStatus on init', () => { + expect(mockEventsSocket.connect).toHaveBeenCalled(); + expect(mockEventsSocket.status$).toHaveBeenCalled(); + }); + + it('should update wsStatus to connected when socket connects', () => { + statusSubject.next('connected'); + expect(store.wsStatus()).toBe('connected'); + }); + + it('should update wsStatus to reconnecting when socket reconnects', () => { + statusSubject.next('connected'); + statusSubject.next('reconnecting'); + expect(store.wsStatus()).toBe('reconnecting'); + }); + + it('should update wsStatus to closed when socket closes', () => { + statusSubject.next('closed'); + expect(store.wsStatus()).toBe('closed'); + }); + + it('should set wsStatus to closed on connect error', () => { + mockEventsSocket.connect = jest.fn().mockReturnValue(throwError(() => new Error('fail'))); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + TestStore, + { provide: DotEventsSocket, useValue: mockEventsSocket } + ] + }); + + store = TestBed.inject(TestStore); + expect(store.wsStatus()).toBe('closed'); + }); + + it('should call destroy on store destroy', () => { + TestBed.resetTestingModule(); + expect(mockEventsSocket.destroy).toHaveBeenCalled(); + }); +}); diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts new file mode 100644 index 000000000000..4b35ce9aad62 --- /dev/null +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts @@ -0,0 +1,77 @@ +import { patchState, signalStoreFeature, withHooks, withMethods, withState } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { merge, Observable, pipe } from 'rxjs'; + +import { inject } from '@angular/core'; + +import { switchMap, tap } from 'rxjs/operators'; + +import { DotEventsSocket, WebSocketStatus } from '@dotcms/data-access'; + +export interface WebSocketState { + wsStatus: WebSocketStatus; +} + +const initialWebSocketState: WebSocketState = { + wsStatus: 'connecting' +}; + +/** + * Store feature that manages the WebSocket connection lifecycle and exposes + * its status as a signal. + * + * - Starts the connection automatically in `onInit` + * - Destroys the connection in `onDestroy` + * - Keeps `wsStatus` in sync via `status$()` + * + * Consumers read `globalStore.wsStatus()` — the UI indicator only shows when not 'connected'. + */ +export function withWebSocket() { + return signalStoreFeature( + withState(initialWebSocketState), + withMethods((store, eventsSocket = inject(DotEventsSocket)) => ({ + startConnection: rxMethod( + pipe(switchMap(() => eventsSocket.connect())) + ), + trackStatus: rxMethod( + pipe( + switchMap(() => + eventsSocket.status$().pipe( + tap((wsStatus) => patchState(store, { wsStatus })) + ) + ) + ) + ), + /** + * Observable that emits when the backend sends UPDATE_PORTLET_LAYOUTS. + * Use this instead of the deprecated DotcmsEventsService. + */ + portletLayoutUpdated$: (): Observable => + eventsSocket.on('UPDATE_PORTLET_LAYOUTS'), + + /** + * Observable that emits whenever a site is created, published, + * archived, unarchived, or updated. Use this to refresh site lists. + */ + siteEvents$: (): Observable => + merge( + eventsSocket.on('SAVE_SITE'), + eventsSocket.on('PUBLISH_SITE'), + eventsSocket.on('UN_PUBLISH_SITE'), + eventsSocket.on('UPDATE_SITE'), + eventsSocket.on('ARCHIVE_SITE'), + eventsSocket.on('UN_ARCHIVE_SITE'), + eventsSocket.on('DELETE_SITE') + ) + })), + withHooks({ + onInit(store) { + store.startConnection(); + store.trackStatus(); + }, + onDestroy(_store, eventsSocket = inject(DotEventsSocket)) { + eventsSocket.destroy(); + } + }) + ); +} diff --git a/core-web/libs/global-store/src/lib/store.spec.ts b/core-web/libs/global-store/src/lib/store.spec.ts index df5854b15168..5620205f03b1 100644 --- a/core-web/libs/global-store/src/lib/store.spec.ts +++ b/core-web/libs/global-store/src/lib/store.spec.ts @@ -1,7 +1,12 @@ import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; +import { Subject, of } from 'rxjs'; -import { DotCurrentUserService, DotSiteService, DotSystemConfigService } from '@dotcms/data-access'; +import { + DotCurrentUserService, + DotEventsSocket, + DotSiteService, + DotSystemConfigService +} from '@dotcms/data-access'; import { GlobalStore } from './store'; import { mockSiteEntity } from './store.mock'; @@ -15,7 +20,12 @@ describe('GlobalStore', () => { providers: [ mockProvider(DotCurrentUserService), mockProvider(DotSiteService), - mockProvider(DotSystemConfigService) + mockProvider(DotSystemConfigService), + mockProvider(DotEventsSocket, { + connect: () => of({}), + status$: () => new Subject(), + on: () => new Subject() + }) ] }); diff --git a/core-web/libs/global-store/src/lib/store.ts b/core-web/libs/global-store/src/lib/store.ts index f99f96b03713..7307297eaec0 100644 --- a/core-web/libs/global-store/src/lib/store.ts +++ b/core-web/libs/global-store/src/lib/store.ts @@ -22,6 +22,7 @@ import { withBreadcrumbs } from './features/breadcrumb/breadcrumb.feature'; import { withMenu } from './features/menu/with-menu.feature'; import { withSystem } from './features/with-system/with-system.feature'; import { withUser } from './features/with-user/with-user.feature'; +import { withWebSocket } from './features/with-websocket/with-websocket.feature'; /** * Represents the global application state. @@ -85,6 +86,7 @@ export const GlobalStore = signalStore( { providedIn: 'root' }, withState(initialState), withSystem(), + withWebSocket(), withComputed(({ siteDetails }) => ({ /** * Computed signal that returns the current site identifier. diff --git a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts index ae386e1ebcdf..aa994bf5c465 100644 --- a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts @@ -1,4 +1,5 @@ import { signalState, patchState } from '@ngrx/signals'; +import { merge, Subject, Subscription } from 'rxjs'; import { CommonModule } from '@angular/common'; import { @@ -24,7 +25,9 @@ import { InputIconModule } from 'primeng/inputicon'; import { InputTextModule } from 'primeng/inputtext'; import { SelectLazyLoadEvent, SelectModule, Select } from 'primeng/select'; -import { DotSiteService } from '@dotcms/data-access'; +import { debounceTime, map } from 'rxjs/operators'; + +import { DotEventsSocket, DotSiteService } from '@dotcms/data-access'; import { DotSite } from '@dotcms/dotcms-models'; interface ParsedSelectLazyLoadEvent extends SelectLazyLoadEvent { @@ -64,6 +67,9 @@ interface DotSiteState { filterValue: string; } +/** Events that mean the site is no longer accessible — switch to default when selected. */ +const SITE_UNAVAILABLE_EVENTS = new Set(['ARCHIVE_SITE', 'UN_PUBLISH_SITE', 'DELETE_SITE']); + @Component({ selector: 'dot-site', imports: [ @@ -93,6 +99,9 @@ interface DotSiteState { }) export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy { private siteService = inject(DotSiteService); + private eventsSocket = inject(DotEventsSocket); + private siteEventsSub: Subscription | null = null; + private readonly siteListRefresh$ = new Subject(); @HostListener('focus') onHostFocus(): void { @@ -268,6 +277,31 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy if (this.$state.sites().length === 0) { this.onLazyLoad({ first: 0, last: this.pageSize - 1 }); } + + // Debounce list refresh so rapid bursts of site events only trigger one reload + this.siteEventsSub = this.siteListRefresh$ + .pipe(debounceTime(300)) + .subscribe(() => this.resetFilter()); + + const tagEvent = (event: string) => + this.eventsSocket.on<{ identifier: string }>(event).pipe( + map((data) => ({ ...data, event })) + ); + + this.siteEventsSub.add( + merge( + tagEvent('SAVE_SITE'), + tagEvent('PUBLISH_SITE'), + tagEvent('UPDATE_SITE'), + tagEvent('ARCHIVE_SITE'), + tagEvent('UN_ARCHIVE_SITE'), + tagEvent('UN_PUBLISH_SITE'), + tagEvent('DELETE_SITE') + ).subscribe((siteData) => { + this.siteListRefresh$.next(); + this.refreshSelectedSite(siteData); + }) + ); } ngOnDestroy(): void { @@ -275,6 +309,9 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy clearTimeout(this.filterDebounceTimeout); this.filterDebounceTimeout = null; } + + this.siteEventsSub?.unsubscribe(); + this.siteListRefresh$.complete(); } // ControlValueAccessor callback functions @@ -627,4 +664,32 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy } }); } + + /** + * Refreshes the selected (pinned) site when a site event fires. + * - Archive/un-archive: skips the GET (site is inaccessible) and switches to default. + * - Other events: re-fetches the site to reflect any name/property changes. + * + * @private + * @param siteData The payload from the site WebSocket event + */ + private refreshSelectedSite(siteData: { identifier: string; event?: string } | undefined): void { + const pinned = this.$state.pinnedOption(); + if (!pinned || siteData?.identifier !== pinned.identifier) { + return; + } + + if (SITE_UNAVAILABLE_EVENTS.has(siteData.event ?? '')) { + this.siteService.switchSite(null).subscribe({ + next: (defaultSite) => this.onSiteChange(defaultSite), + error: () => patchState(this.$state, { pinnedOption: null }) + }); + return; + } + + this.siteService.getSiteById(pinned.identifier).subscribe({ + next: (site) => patchState(this.$state, { pinnedOption: site }), + error: () => patchState(this.$state, { pinnedOption: null }) + }); + } } diff --git a/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java b/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java index 99105f8a4763..9c316f329775 100644 --- a/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java +++ b/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java @@ -56,13 +56,18 @@ public enum SystemEventType { /** * When a site is published */ - PUBLISH_SITE, // todo: not used + PUBLISH_SITE, /** * When a site is updated */ UPDATE_SITE, // todo: not used + /** + * When a site is unpublished (stopped) + */ + UN_PUBLISH_SITE, + /** * When a site is archived */ From 628e100a378203eaaeb5d0f5ab7ae9d6a43e5dd0 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 14:28:48 -0600 Subject: [PATCH 02/36] fix(websockets): address code quality issues from simplify review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix timer subscription leak in scheduleReconnect() — store subscription in reconnectTimer and cancel on destroy() - Guard openSocket() against concurrent calls when socket is CONNECTING - Remove dead getLongPollingURL() method from DotEventsSocketURL - Update spec mock to remove getLongPollingURL reference - Remove invalid test in with-websocket.feature.spec.ts that assumed rxMethod would propagate errors and set wsStatus to closed (rxMethod swallows errors and connect() completes immediately, never throws) - Add dot-events-socket.service.spec.ts (was untracked) - Fix stale JSDoc in refreshSelectedSite() — "archive/un-archive" → "unavailable events" Co-Authored-By: Claude Sonnet 4.6 --- .../dot-websocket/dot-events-socket-url.ts | 4 - .../dot-events-socket.service.spec.ts | 346 ++++++++++++++++++ .../dot-events-socket.service.ts | 11 +- .../with-websocket.feature.spec.ts | 22 +- .../components/dot-site/dot-site.component.ts | 12 +- 5 files changed, 363 insertions(+), 32 deletions(-) create mode 100644 core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts index eee99a51bae2..9358c106768f 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts @@ -13,10 +13,6 @@ export class DotEventsSocketURL { getWebSocketURL(): string { return `${this.useSSL ? 'wss' : 'ws'}://${this.url}`; } - - getLongPollingURL(): string { - return `${this.useSSL ? 'https' : 'http'}://${this.url}`; - } } export const DOT_EVENTS_SOCKET_URL = new InjectionToken('DotEventsSocketURL'); diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts new file mode 100644 index 000000000000..7e15d927485a --- /dev/null +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts @@ -0,0 +1,346 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; + +import { DOT_EVENTS_SOCKET_URL, DotEventsSocketURL } from './dot-events-socket-url'; +import { DotEventsSocket, WebSocketStatus } from './dot-events-socket.service'; + +// --------------------------------------------------------------------------- +// Minimal WebSocket mock — exposes handlers so tests can trigger them +// --------------------------------------------------------------------------- +class MockWebSocket { + static instances: MockWebSocket[] = []; + + onopen: (() => void) | null = null; + onmessage: ((ev: Partial) => void) | null = null; + onclose: ((ev: Partial) => void) | null = null; + onerror: (() => void) | null = null; + readyState = WebSocket.CONNECTING; + + constructor(public url: string) { + MockWebSocket.instances.push(this); + } + + close(): void { + this.readyState = WebSocket.CLOSED; + } + + /** Test helper — simulate a successful connection */ + triggerOpen(): void { + this.readyState = WebSocket.OPEN; + this.onopen?.(); + } + + /** Test helper — simulate a close event */ + triggerClose(code = 1006): void { + this.readyState = WebSocket.CLOSED; + this.onclose?.({ code }); + } + + /** Test helper — simulate an incoming message */ + triggerMessage(data: unknown): void { + this.onmessage?.({ data: JSON.stringify(data) }); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +const WS_URL = 'ws://localhost:8080/api/ws/v1/system/events'; + +const mockSocketURL = { + getWebSocketURL: () => WS_URL +} as DotEventsSocketURL; + +function latestSocket(): MockWebSocket { + return MockWebSocket.instances[MockWebSocket.instances.length - 1]; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- +describe('DotEventsSocket', () => { + let spectator: SpectatorService; + let service: DotEventsSocket; + + const createService = createServiceFactory({ + service: DotEventsSocket, + providers: [{ provide: DOT_EVENTS_SOCKET_URL, useValue: mockSocketURL }] + }); + + beforeEach(() => { + jest.useFakeTimers(); + MockWebSocket.instances = []; + (global as unknown as { WebSocket: unknown }).WebSocket = MockWebSocket; + + spectator = createService(); + service = spectator.service; + }); + + afterEach(() => { + jest.useRealTimers(); + service.destroy(); + }); + + // ----------------------------------------------------------------------- + // connect() + // ----------------------------------------------------------------------- + describe('connect()', () => { + it('should open a WebSocket to the configured URL', () => { + service.connect().subscribe(); + + expect(MockWebSocket.instances.length).toBe(1); + expect(latestSocket().url).toBe(WS_URL); + }); + + it('should emit and complete immediately', (done) => { + let emitted = false; + service.connect().subscribe({ + next: () => { + emitted = true; + }, + complete: () => { + expect(emitted).toBe(true); + done(); + } + }); + }); + + it('should set status to "connecting" on first connect', () => { + const statuses: WebSocketStatus[] = []; + service.status$().subscribe((s) => statuses.push(s)); + + service.connect().subscribe(); + + expect(statuses).toContain('connecting'); + }); + + it('should set status to "connected" when socket opens', () => { + const statuses: WebSocketStatus[] = []; + service.status$().subscribe((s) => statuses.push(s)); + + service.connect().subscribe(); + latestSocket().triggerOpen(); + + expect(statuses[statuses.length - 1]).toBe('connected'); + }); + + it('should reset retryCount on successful open', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + latestSocket().triggerClose(); + jest.advanceTimersByTime(2000); + + latestSocket().triggerOpen(); + + // After reconnect succeeds, status goes back to connected + const statuses: WebSocketStatus[] = []; + service.status$().subscribe((s) => statuses.push(s)); + expect(service.isConnected()).toBe(true); + }); + }); + + // ----------------------------------------------------------------------- + // on() — message filtering + // ----------------------------------------------------------------------- + describe('on()', () => { + it('should emit payload for matching event type', (done) => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + + service.on<{ name: string }>('PUBLISH_SITE').subscribe((data) => { + expect(data).toEqual({ name: 'demo.dotcms.com' }); + done(); + }); + + latestSocket().triggerMessage({ + event: 'PUBLISH_SITE', + payload: { data: { name: 'demo.dotcms.com' } } + }); + }); + + it('should not emit for non-matching event type', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + + const received: unknown[] = []; + service.on('OTHER_EVENT').subscribe((d) => received.push(d)); + + latestSocket().triggerMessage({ + event: 'PUBLISH_SITE', + payload: { data: {} } + }); + + expect(received).toHaveLength(0); + }); + + it('should ignore unparseable messages', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + + const received: unknown[] = []; + service.on('ANY').subscribe((d) => received.push(d)); + + // Trigger invalid JSON via onmessage directly + latestSocket().onmessage?.({ data: 'not-json' }); + + expect(received).toHaveLength(0); + }); + }); + + // ----------------------------------------------------------------------- + // messages() + // ----------------------------------------------------------------------- + describe('messages()', () => { + it('should emit all raw messages', (done) => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + + service.messages().subscribe((msg) => { + expect(msg.event).toBe('UPDATE_SITE'); + done(); + }); + + latestSocket().triggerMessage({ event: 'UPDATE_SITE', payload: { data: {} } }); + }); + }); + + // ----------------------------------------------------------------------- + // isConnected() + // ----------------------------------------------------------------------- + describe('isConnected()', () => { + it('should return false before connection opens', () => { + service.connect().subscribe(); + + expect(service.isConnected()).toBe(false); + }); + + it('should return true after socket opens', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + + expect(service.isConnected()).toBe(true); + }); + + it('should return false after destroy', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + service.destroy(); + + expect(service.isConnected()).toBe(false); + }); + }); + + // ----------------------------------------------------------------------- + // Reconnection + // ----------------------------------------------------------------------- + describe('reconnection', () => { + it('should reconnect after socket closes unexpectedly', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + latestSocket().triggerClose(1006); + + jest.advanceTimersByTime(3000); + + expect(MockWebSocket.instances.length).toBe(2); + }); + + it('should set status to "reconnecting" after first disconnect', () => { + const statuses: WebSocketStatus[] = []; + service.status$().subscribe((s) => statuses.push(s)); + + service.connect().subscribe(); + latestSocket().triggerOpen(); + latestSocket().triggerClose(1006); + + expect(statuses).toContain('reconnecting'); + }); + + it('should NOT reconnect after destroy', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + service.destroy(); + latestSocket().triggerClose(1006); + + jest.advanceTimersByTime(5000); + + expect(MockWebSocket.instances.length).toBe(1); + }); + + it('should not reconnect on close code 1001 (going away)', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + latestSocket().triggerClose(1001); + + jest.advanceTimersByTime(5000); + + expect(MockWebSocket.instances.length).toBe(1); + }); + + it('should use exponential backoff — second retry waits longer than first', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + + // First disconnect + latestSocket().triggerClose(1006); + const countAfterFirst = MockWebSocket.instances.length; + jest.advanceTimersByTime(2000); // enough for first retry (1s base + jitter) + const countAfterFirstRetry = MockWebSocket.instances.length; + + // Second disconnect + latestSocket().triggerClose(1006); + jest.advanceTimersByTime(2000); // NOT enough for second retry (2s base + jitter) + const countAfterShortWait = MockWebSocket.instances.length; + + jest.advanceTimersByTime(3000); // now enough + const countAfterLongWait = MockWebSocket.instances.length; + + expect(countAfterFirstRetry).toBeGreaterThan(countAfterFirst); + expect(countAfterShortWait).toBe(countAfterFirstRetry); // no new socket yet + expect(countAfterLongWait).toBeGreaterThan(countAfterShortWait); + }); + }); + + // ----------------------------------------------------------------------- + // destroy() + // ----------------------------------------------------------------------- + describe('destroy()', () => { + it('should set status to "closed"', () => { + const statuses: WebSocketStatus[] = []; + service.status$().subscribe((s) => statuses.push(s)); + + service.connect().subscribe(); + latestSocket().triggerOpen(); + service.destroy(); + + expect(statuses[statuses.length - 1]).toBe('closed'); + }); + + it('should close the underlying socket', () => { + service.connect().subscribe(); + latestSocket().triggerOpen(); + const socket = latestSocket(); + service.destroy(); + + expect(socket.readyState).toBe(WebSocket.CLOSED); + }); + + it('should do nothing if called without a prior connect()', () => { + expect(() => service.destroy()).not.toThrow(); + }); + }); + + // ----------------------------------------------------------------------- + // status$() deduplication + // ----------------------------------------------------------------------- + describe('status$()', () => { + it('should not emit duplicate statuses', () => { + const statuses: WebSocketStatus[] = []; + service.status$().subscribe((s) => statuses.push(s)); + + service.connect().subscribe(); + // Both openSocket calls set 'connecting' — only one emission expected + service.connect().subscribe(); + + expect(statuses.filter((s) => s === 'connecting')).toHaveLength(1); + }); + }); +}); diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts index ea40d5a83bf8..a71fef685e5c 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject, Observable, Subject, timer } from 'rxjs'; +import { BehaviorSubject, Observable, Subject, Subscription, timer } from 'rxjs'; import { Injectable, inject } from '@angular/core'; @@ -26,6 +26,7 @@ export class DotEventsSocket { private status: WebSocketStatus = 'connecting'; private retryCount = 0; private destroyed = false; + private reconnectTimer: Subscription | null = null; private readonly _message = new Subject(); private readonly _status = new BehaviorSubject('connecting'); @@ -51,6 +52,8 @@ export class DotEventsSocket { /** Closes the connection permanently (e.g. on logout). */ destroy(): void { this.destroyed = true; + this.reconnectTimer?.unsubscribe(); + this.reconnectTimer = null; this.setStatus('closed'); this.socket?.close(); this.socket = null; @@ -79,7 +82,7 @@ export class DotEventsSocket { } private openSocket(): void { - if (this.destroyed) { + if (this.destroyed || this.socket?.readyState === WebSocket.CONNECTING) { return; } @@ -129,7 +132,9 @@ export class DotEventsSocket { this.retryCount++; this.setStatus('reconnecting'); - timer(this.calculateDelay()).subscribe(() => { + this.reconnectTimer?.unsubscribe(); + this.reconnectTimer = timer(this.calculateDelay()).subscribe(() => { + this.reconnectTimer = null; if (!this.destroyed) { this.openSocket(); } diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts index 1972ab287e38..41d18a6f4c1e 100644 --- a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.spec.ts @@ -1,5 +1,5 @@ import { signalStore, withState } from '@ngrx/signals'; -import { Subject, of, throwError } from 'rxjs'; +import { Subject, of } from 'rxjs'; import { TestBed } from '@angular/core/testing'; @@ -25,10 +25,7 @@ describe('withWebSocket Feature', () => { }; TestBed.configureTestingModule({ - providers: [ - TestStore, - { provide: DotEventsSocket, useValue: mockEventsSocket } - ] + providers: [TestStore, { provide: DotEventsSocket, useValue: mockEventsSocket }] }); store = TestBed.inject(TestStore); @@ -59,21 +56,6 @@ describe('withWebSocket Feature', () => { expect(store.wsStatus()).toBe('closed'); }); - it('should set wsStatus to closed on connect error', () => { - mockEventsSocket.connect = jest.fn().mockReturnValue(throwError(() => new Error('fail'))); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - TestStore, - { provide: DotEventsSocket, useValue: mockEventsSocket } - ] - }); - - store = TestBed.inject(TestStore); - expect(store.wsStatus()).toBe('closed'); - }); - it('should call destroy on store destroy', () => { TestBed.resetTestingModule(); expect(mockEventsSocket.destroy).toHaveBeenCalled(); diff --git a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts index aa994bf5c465..ee7aeb8b0eff 100644 --- a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts @@ -284,9 +284,9 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy .subscribe(() => this.resetFilter()); const tagEvent = (event: string) => - this.eventsSocket.on<{ identifier: string }>(event).pipe( - map((data) => ({ ...data, event })) - ); + this.eventsSocket + .on<{ identifier: string }>(event) + .pipe(map((data) => ({ ...data, event }))); this.siteEventsSub.add( merge( @@ -667,13 +667,15 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy /** * Refreshes the selected (pinned) site when a site event fires. - * - Archive/un-archive: skips the GET (site is inaccessible) and switches to default. + * - Unavailable events (archive, stop, delete): switches to the default site. * - Other events: re-fetches the site to reflect any name/property changes. * * @private * @param siteData The payload from the site WebSocket event */ - private refreshSelectedSite(siteData: { identifier: string; event?: string } | undefined): void { + private refreshSelectedSite( + siteData: { identifier: string; event?: string } | undefined + ): void { const pinned = this.$state.pinnedOption(); if (!pinned || siteData?.identifier !== pinned.identifier) { return; From f7d6ce4637d54f59f20b52a7fd4c67b5f92b7ed8 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 14:39:16 -0600 Subject: [PATCH 03/36] style: apply prettier formatting to websocket-related files Co-Authored-By: Claude Sonnet 4.6 --- core-web/apps/dotcms-ui/src/app/providers.ts | 1 - .../dot-navigation/services/dot-navigation.service.ts | 4 +--- .../features/with-websocket/with-websocket.feature.ts | 10 ++++------ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index b699b08ea147..41a88a97e33f 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -80,7 +80,6 @@ const dotEventSocketURLFactory = () => { ); }; - const PROVIDERS: Provider[] = [ { provide: LOCATION_TOKEN, useValue: window.location }, EmaAppConfigurationService, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts index eae7088c4dea..e09650ed04e9 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts @@ -81,9 +81,7 @@ export class DotNavigationService { // Handle portlet layout updates from the global store WebSocket feature this.#globalStore .portletLayoutUpdated$() - .pipe( - switchMap(() => this.dotMenuService.reloadMenu().pipe(take(1))) - ) + .pipe(switchMap(() => this.dotMenuService.reloadMenu().pipe(take(1)))) .subscribe((menus: DotMenu[]) => { this.registerDynamicRoutes(menus); this.#globalStore.loadMenu(menus); diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts index 4b35ce9aad62..928419b671d6 100644 --- a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts @@ -30,15 +30,13 @@ export function withWebSocket() { return signalStoreFeature( withState(initialWebSocketState), withMethods((store, eventsSocket = inject(DotEventsSocket)) => ({ - startConnection: rxMethod( - pipe(switchMap(() => eventsSocket.connect())) - ), + startConnection: rxMethod(pipe(switchMap(() => eventsSocket.connect()))), trackStatus: rxMethod( pipe( switchMap(() => - eventsSocket.status$().pipe( - tap((wsStatus) => patchState(store, { wsStatus })) - ) + eventsSocket + .status$() + .pipe(tap((wsStatus) => patchState(store, { wsStatus }))) ) ) ), From b9134410e813fc246d4c88a1c6d1b74db2a5f047 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 15:00:09 -0600 Subject: [PATCH 04/36] refactor(dot-site): remove intermediate Subject from site event handling Replace the siteListRefresh$ Subject + two subscriptions pattern with a single pipe using tap + debounceTime directly on the merged site events observable. Co-Authored-By: Claude Sonnet 4.6 --- .../components/dot-site/dot-site.component.ts | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts index ee7aeb8b0eff..8411b52ea015 100644 --- a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts @@ -1,5 +1,5 @@ import { signalState, patchState } from '@ngrx/signals'; -import { merge, Subject, Subscription } from 'rxjs'; +import { merge, Subscription } from 'rxjs'; import { CommonModule } from '@angular/common'; import { @@ -25,7 +25,7 @@ import { InputIconModule } from 'primeng/inputicon'; import { InputTextModule } from 'primeng/inputtext'; import { SelectLazyLoadEvent, SelectModule, Select } from 'primeng/select'; -import { debounceTime, map } from 'rxjs/operators'; +import { debounceTime, map, tap } from 'rxjs/operators'; import { DotEventsSocket, DotSiteService } from '@dotcms/data-access'; import { DotSite } from '@dotcms/dotcms-models'; @@ -101,7 +101,6 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy private siteService = inject(DotSiteService); private eventsSocket = inject(DotEventsSocket); private siteEventsSub: Subscription | null = null; - private readonly siteListRefresh$ = new Subject(); @HostListener('focus') onHostFocus(): void { @@ -278,30 +277,27 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy this.onLazyLoad({ first: 0, last: this.pageSize - 1 }); } - // Debounce list refresh so rapid bursts of site events only trigger one reload - this.siteEventsSub = this.siteListRefresh$ - .pipe(debounceTime(300)) - .subscribe(() => this.resetFilter()); - const tagEvent = (event: string) => this.eventsSocket .on<{ identifier: string }>(event) .pipe(map((data) => ({ ...data, event }))); - this.siteEventsSub.add( - merge( - tagEvent('SAVE_SITE'), - tagEvent('PUBLISH_SITE'), - tagEvent('UPDATE_SITE'), - tagEvent('ARCHIVE_SITE'), - tagEvent('UN_ARCHIVE_SITE'), - tagEvent('UN_PUBLISH_SITE'), - tagEvent('DELETE_SITE') - ).subscribe((siteData) => { - this.siteListRefresh$.next(); - this.refreshSelectedSite(siteData); - }) - ); + // Each event immediately refreshes the selected site; list reload is debounced + // to coalesce rapid bursts into a single resetFilter() call. + this.siteEventsSub = merge( + tagEvent('SAVE_SITE'), + tagEvent('PUBLISH_SITE'), + tagEvent('UPDATE_SITE'), + tagEvent('ARCHIVE_SITE'), + tagEvent('UN_ARCHIVE_SITE'), + tagEvent('UN_PUBLISH_SITE'), + tagEvent('DELETE_SITE') + ) + .pipe( + tap((siteData) => this.refreshSelectedSite(siteData)), + debounceTime(300) + ) + .subscribe(() => this.resetFilter()); } ngOnDestroy(): void { @@ -311,7 +307,6 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy } this.siteEventsSub?.unsubscribe(); - this.siteListRefresh$.complete(); } // ControlValueAccessor callback functions From cbcc6e2e747f008151de52a7e0cf4523ff11b7c5 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 15:19:41 -0600 Subject: [PATCH 05/36] test(dot-site): add WebSocket site event tests and fix DotEventsSocket mock - Add 'WebSocket site events' describe block to dot-site.component.spec.ts covering: debounced list refresh, re-fetch on update, no-op for other sites, switch-to-default on ARCHIVE/UN_PUBLISH/DELETE, error fallbacks, and unsubscribe on destroy - Add mockProvider(DotEventsSocket) to dot-site.component.spec.ts factories - Fix dot-theme.component.spec.ts: add mockProvider(DotEventsSocket) since DotThemeComponent embeds DotSiteComponent which now injects DotEventsSocket Co-Authored-By: Claude Sonnet 4.6 --- .../dot-site/dot-site.component.spec.ts | 156 +++++++++++++++++- .../dot-theme/dot-theme.component.spec.ts | 7 +- 2 files changed, 157 insertions(+), 6 deletions(-) diff --git a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts index e5bc15a11fd1..a2d447e8fdb2 100644 --- a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts @@ -7,7 +7,7 @@ import { SpyObject } from '@ngneat/spectator/jest'; import { patchState } from '@ngrx/signals'; -import { of, throwError } from 'rxjs'; +import { Subject, of, throwError } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -17,7 +17,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { Select, SelectLazyLoadEvent } from 'primeng/select'; -import { DotSiteService } from '@dotcms/data-access'; +import { DotEventsSocket, DotSiteService } from '@dotcms/data-access'; import { DotPagination, DotSite } from '@dotcms/dotcms-models'; import { DotSiteComponent } from './dot-site.component'; @@ -65,7 +65,12 @@ describe('DotSiteComponent', () => { const createComponent = createComponentFactory({ component: DotSiteComponent, imports: [ReactiveFormsModule], - providers: [mockProvider(DotSiteService), provideHttpClient(), provideHttpClientTesting()] + providers: [ + mockProvider(DotSiteService), + mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), + provideHttpClient(), + provideHttpClientTesting() + ] }); beforeEach(() => { @@ -1009,6 +1014,144 @@ describe('DotSiteComponent', () => { expect(options.find((s) => s.identifier === 'site99')).toBeTruthy(); })); }); + + describe('WebSocket site events', () => { + let eventsSocket: SpyObject; + let siteEventSubjects: Record>; + + beforeEach(() => { + eventsSocket = spectator.inject(DotEventsSocket, true); + siteEventSubjects = {}; + + eventsSocket.on.mockImplementation((eventType: string) => { + siteEventSubjects[eventType] = new Subject<{ identifier: string }>(); + return siteEventSubjects[eventType].asObservable(); + }); + + spectator.detectChanges(); + }); + + it('should call resetFilter after debounce when any site event fires', fakeAsync(() => { + jest.clearAllMocks(); + siteEventSubjects['PUBLISH_SITE'].next({ identifier: 'site1' }); + tick(299); + expect(siteService.getSites).not.toHaveBeenCalled(); + + tick(1); + expect(siteService.getSites).toHaveBeenCalled(); + })); + + it('should debounce multiple rapid events into a single resetFilter call', fakeAsync(() => { + jest.clearAllMocks(); + siteEventSubjects['SAVE_SITE'].next({ identifier: 'site1' }); + siteEventSubjects['UPDATE_SITE'].next({ identifier: 'site2' }); + siteEventSubjects['PUBLISH_SITE'].next({ identifier: 'site3' }); + tick(300); + + expect(siteService.getSites).toHaveBeenCalledTimes(1); + })); + + it('should re-fetch the selected site when a non-unavailable event fires for it', fakeAsync(() => { + patchState(spectator.component.$state, { pinnedOption: mockSites[0] }); + siteService.getSiteById.mockReturnValue(of(mockSites[0])); + jest.clearAllMocks(); + + siteEventSubjects['UPDATE_SITE'].next({ identifier: 'site1' }); + + expect(siteService.getSiteById).toHaveBeenCalledWith('site1'); + tick(300); + })); + + it('should NOT re-fetch when the event is for a different site', fakeAsync(() => { + patchState(spectator.component.$state, { pinnedOption: mockSites[0] }); + jest.clearAllMocks(); + + siteEventSubjects['UPDATE_SITE'].next({ identifier: 'site2' }); + tick(300); + + expect(siteService.getSiteById).not.toHaveBeenCalled(); + })); + + it('should switch to default site when ARCHIVE_SITE fires for the selected site', fakeAsync(() => { + const defaultSite: DotSite = { + hostname: 'default.com', + identifier: 'default', + archived: false, + aliases: null + }; + patchState(spectator.component.$state, { pinnedOption: mockSites[0] }); + siteService.switchSite = jest.fn().mockReturnValue(of(defaultSite)); + jest.clearAllMocks(); + + siteEventSubjects['ARCHIVE_SITE'].next({ identifier: 'site1' }); + tick(300); + + expect(siteService.switchSite).toHaveBeenCalledWith(null); + expect(spectator.component.$state.pinnedOption()).toEqual(defaultSite); + })); + + it('should switch to default site when UN_PUBLISH_SITE fires for the selected site', fakeAsync(() => { + const defaultSite: DotSite = { + hostname: 'default.com', + identifier: 'default', + archived: false, + aliases: null + }; + patchState(spectator.component.$state, { pinnedOption: mockSites[0] }); + siteService.switchSite = jest.fn().mockReturnValue(of(defaultSite)); + + siteEventSubjects['UN_PUBLISH_SITE'].next({ identifier: 'site1' }); + tick(300); + + expect(siteService.switchSite).toHaveBeenCalledWith(null); + expect(spectator.component.value()).toBe('default'); + })); + + it('should switch to default site when DELETE_SITE fires for the selected site', fakeAsync(() => { + const defaultSite: DotSite = { + hostname: 'default.com', + identifier: 'default', + archived: false, + aliases: null + }; + patchState(spectator.component.$state, { pinnedOption: mockSites[0] }); + siteService.switchSite = jest.fn().mockReturnValue(of(defaultSite)); + + siteEventSubjects['DELETE_SITE'].next({ identifier: 'site1' }); + tick(300); + + expect(siteService.switchSite).toHaveBeenCalledWith(null); + })); + + it('should set pinnedOption to null when switchSite fails', fakeAsync(() => { + patchState(spectator.component.$state, { pinnedOption: mockSites[0] }); + siteService.switchSite = jest + .fn() + .mockReturnValue(throwError(() => new Error('switchSite failed'))); + + siteEventSubjects['ARCHIVE_SITE'].next({ identifier: 'site1' }); + tick(300); + + expect(spectator.component.$state.pinnedOption()).toBeNull(); + })); + + it('should set pinnedOption to null when getSiteById fails on re-fetch', fakeAsync(() => { + patchState(spectator.component.$state, { pinnedOption: mockSites[0] }); + siteService.getSiteById.mockReturnValue(throwError(() => new Error('not found'))); + + siteEventSubjects['UPDATE_SITE'].next({ identifier: 'site1' }); + tick(300); + + expect(spectator.component.$state.pinnedOption()).toBeNull(); + })); + + it('should unsubscribe from site events on destroy', () => { + const sub = spectator.component['siteEventsSub']; + const unsubscribeSpy = jest.spyOn(sub, 'unsubscribe'); + spectator.component.ngOnDestroy(); + expect(unsubscribeSpy).toHaveBeenCalled(); + }); + }); }); describe('DotSiteComponent - ControlValueAccessor Integration', () => { @@ -1016,7 +1159,12 @@ describe('DotSiteComponent - ControlValueAccessor Integration', () => { component: DotSiteComponent, host: FormHostComponent, imports: [ReactiveFormsModule], - providers: [mockProvider(DotSiteService), provideHttpClient(), provideHttpClientTesting()], + providers: [ + mockProvider(DotSiteService), + mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), + provideHttpClient(), + provideHttpClientTesting() + ], detectChanges: false }); diff --git a/core-web/libs/ui/src/lib/components/dot-theme/dot-theme.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-theme/dot-theme.component.spec.ts index 9c3e72349339..e09f511d8ad2 100644 --- a/core-web/libs/ui/src/lib/components/dot-theme/dot-theme.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-theme/dot-theme.component.spec.ts @@ -6,7 +6,7 @@ import { SpectatorHost, SpyObject } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; +import { Subject, of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -17,7 +17,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { Button } from 'primeng/button'; import { DataView } from 'primeng/dataview'; -import { DotThemesService } from '@dotcms/data-access'; +import { DotEventsSocket, DotThemesService } from '@dotcms/data-access'; import { DotPagination, DotTheme } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; @@ -88,6 +88,7 @@ describe('DotThemeComponent', () => { imports: [ReactiveFormsModule], providers: [ mockProvider(DotThemesService), + mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), provideHttpClient(), provideHttpClientTesting(), { @@ -349,6 +350,7 @@ describe('DotThemeComponent - ControlValueAccessor writeValue', () => { imports: [ReactiveFormsModule], providers: [ mockProvider(DotThemesService), + mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), provideHttpClient(), provideHttpClientTesting(), { @@ -456,6 +458,7 @@ describe('DotThemeComponent - ControlValueAccessor Integration', () => { imports: [ReactiveFormsModule], providers: [ mockProvider(DotThemesService), + mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), provideHttpClient(), provideHttpClientTesting(), { From 975fc99be8e53c1fbf59276650934c754a70add8 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 15:26:44 -0600 Subject: [PATCH 06/36] refactor(websockets): replace DotEventsSocketURL class with plain string token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class existed only to build a ws/wss URL from hostname + SSL flag. Replace with InjectionToken — the factory in providers.ts returns the URL string directly, and DotEventsSocket injects it as a plain string. Co-Authored-By: Claude Sonnet 4.6 --- core-web/apps/dotcms-ui/src/app/providers.ts | 7 ++----- .../lib/dot-websocket/dot-events-socket-url.ts | 17 +---------------- .../dot-events-socket.service.spec.ts | 6 ++---- .../dot-websocket/dot-events-socket.service.ts | 2 +- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index 41a88a97e33f..190cee8e8ba9 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -15,7 +15,6 @@ import { DotCurrentUserService, DotEventsService, DotEventsSocket, - DotEventsSocketURL, DotFormatDateService, DotGenerateSecurePasswordService, DotGlobalMessageService, @@ -74,10 +73,8 @@ import { DotLoginPageStateService } from './view/components/login/shared/service export const LOCATION_TOKEN = new InjectionToken('Window location object'); const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; + return `${protocol}://${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`; }; const PROVIDERS: Provider[] = [ diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts index 9358c106768f..7916137c2b55 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts @@ -1,18 +1,3 @@ import { InjectionToken } from '@angular/core'; -/** - * Represents the URL configuration for the WebSocket event endpoint. - * Provide this via DotEventsSocketURL injection token. - */ -export class DotEventsSocketURL { - constructor( - private url: string, - private useSSL: boolean - ) {} - - getWebSocketURL(): string { - return `${this.useSSL ? 'wss' : 'ws'}://${this.url}`; - } -} - -export const DOT_EVENTS_SOCKET_URL = new InjectionToken('DotEventsSocketURL'); +export const DOT_EVENTS_SOCKET_URL = new InjectionToken('DotEventsSocketURL'); diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts index 7e15d927485a..0a425d76e264 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts @@ -1,6 +1,6 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; -import { DOT_EVENTS_SOCKET_URL, DotEventsSocketURL } from './dot-events-socket-url'; +import { DOT_EVENTS_SOCKET_URL } from './dot-events-socket-url'; import { DotEventsSocket, WebSocketStatus } from './dot-events-socket.service'; // --------------------------------------------------------------------------- @@ -46,9 +46,7 @@ class MockWebSocket { // --------------------------------------------------------------------------- const WS_URL = 'ws://localhost:8080/api/ws/v1/system/events'; -const mockSocketURL = { - getWebSocketURL: () => WS_URL -} as DotEventsSocketURL; +const mockSocketURL = WS_URL; function latestSocket(): MockWebSocket { return MockWebSocket.instances[MockWebSocket.instances.length - 1]; diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts index a71fef685e5c..40b4b4089d25 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts @@ -89,7 +89,7 @@ export class DotEventsSocket { this.setStatus(this.retryCount === 0 ? 'connecting' : 'reconnecting'); try { - this.socket = new WebSocket(this.socketURL.getWebSocketURL()); + this.socket = new WebSocket(this.socketURL); } catch { this.scheduleReconnect(); return; From 2540751216aaed6be3f53cb3927b9096a7055b61 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 15:53:41 -0600 Subject: [PATCH 07/36] refactor(websockets): simplify WS URL construction using window.location.host Use window.location.host (hostname + port combined) instead of manually concatenating hostname and port separately. Co-Authored-By: Claude Sonnet 4.6 --- core-web/apps/dotcms-ui/src/app/providers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index 190cee8e8ba9..552dc1aae896 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -73,8 +73,8 @@ import { DotLoginPageStateService } from './view/components/login/shared/service export const LOCATION_TOKEN = new InjectionToken('Window location object'); const dotEventSocketURLFactory = () => { - const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; - return `${protocol}://${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/api/ws/v1/system/events`; }; const PROVIDERS: Provider[] = [ From 92678500adbfc14dc06c99486dd07eaeeadf5ad4 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 16:19:05 -0600 Subject: [PATCH 08/36] refactor(websockets): inline WebSocket URL into service, remove token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DotEventsSocket now derives its URL directly from window.location — no injection token or external configuration needed. Removes DOT_EVENTS_SOCKET_URL token, DotEventsSocketURL file, and the factory in providers.ts entirely. Co-Authored-By: Claude Sonnet 4.6 --- core-web/apps/dotcms-ui/src/app/providers.ts | 7 ------- core-web/libs/data-access/src/index.ts | 1 - .../src/lib/dot-websocket/dot-events-socket-url.ts | 3 --- .../lib/dot-websocket/dot-events-socket.service.spec.ts | 8 +++----- .../src/lib/dot-websocket/dot-events-socket.service.ts | 8 +++++--- 5 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index 552dc1aae896..d5acd01a975e 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -5,7 +5,6 @@ import { ConfirmationService } from 'primeng/api'; import { CanDeactivateGuardService, - DOT_EVENTS_SOCKET_URL, DotAlertConfirmService, DotAppsService, DotContentletService, @@ -72,11 +71,6 @@ import { DotLoginPageStateService } from './view/components/login/shared/service export const LOCATION_TOKEN = new InjectionToken('Window location object'); -const dotEventSocketURLFactory = () => { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - return `${protocol}//${window.location.host}/api/ws/v1/system/events`; -}; - const PROVIDERS: Provider[] = [ { provide: LOCATION_TOKEN, useValue: window.location }, EmaAppConfigurationService, @@ -126,7 +120,6 @@ const PROVIDERS: Provider[] = [ DotcmsEventsService, LoggerService, LoginService, - { provide: DOT_EVENTS_SOCKET_URL, useFactory: dotEventSocketURLFactory }, DotEventsSocket, StringUtils, UserModel, diff --git a/core-web/libs/data-access/src/index.ts b/core-web/libs/data-access/src/index.ts index 3c21bb47a844..790dc5bd0b8f 100644 --- a/core-web/libs/data-access/src/index.ts +++ b/core-web/libs/data-access/src/index.ts @@ -74,5 +74,4 @@ export * from './lib/dot-content-drive/dot-content-drive.service'; export * from './lib/dot-usage/dot-usage.service'; export * from './lib/dot-websocket/dot-events-socket.service'; -export * from './lib/dot-websocket/dot-events-socket-url'; export * from './lib/dot-websocket/dot-event-message.model'; diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts deleted file mode 100644 index 7916137c2b55..000000000000 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket-url.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -export const DOT_EVENTS_SOCKET_URL = new InjectionToken('DotEventsSocketURL'); diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts index 0a425d76e264..4edc1187c437 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts @@ -1,6 +1,5 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; -import { DOT_EVENTS_SOCKET_URL } from './dot-events-socket-url'; import { DotEventsSocket, WebSocketStatus } from './dot-events-socket.service'; // --------------------------------------------------------------------------- @@ -44,9 +43,9 @@ class MockWebSocket { // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -const WS_URL = 'ws://localhost:8080/api/ws/v1/system/events'; -const mockSocketURL = WS_URL; +// Jest's jsdom sets window.location to http://localhost/, so the service builds: +const WS_URL = 'ws://localhost/api/ws/v1/system/events'; function latestSocket(): MockWebSocket { return MockWebSocket.instances[MockWebSocket.instances.length - 1]; @@ -60,8 +59,7 @@ describe('DotEventsSocket', () => { let service: DotEventsSocket; const createService = createServiceFactory({ - service: DotEventsSocket, - providers: [{ provide: DOT_EVENTS_SOCKET_URL, useValue: mockSocketURL }] + service: DotEventsSocket }); beforeEach(() => { diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts index 40b4b4089d25..3bea1d284c8c 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts @@ -1,11 +1,10 @@ import { BehaviorSubject, Observable, Subject, Subscription, timer } from 'rxjs'; -import { Injectable, inject } from '@angular/core'; +import { Injectable } from '@angular/core'; import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { DotEventMessage } from './dot-event-message.model'; -import { DOT_EVENTS_SOCKET_URL } from './dot-events-socket-url'; export type WebSocketStatus = 'connecting' | 'reconnecting' | 'connected' | 'closed'; @@ -20,7 +19,10 @@ export type WebSocketStatus = 'connecting' | 'reconnecting' | 'connected' | 'clo */ @Injectable({ providedIn: 'root' }) export class DotEventsSocket { - private readonly socketURL = inject(DOT_EVENTS_SOCKET_URL); + private readonly socketURL = (() => { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/api/ws/v1/system/events`; + })(); private socket: WebSocket | null = null; private status: WebSocketStatus = 'connecting'; From 765ceff6b4a867363d986df7f51061b0364e7a9b Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 17:16:23 -0600 Subject: [PATCH 09/36] feat(websockets): handle SWITCH_SITE event in toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add switchSiteEvent$() to withWebSocket feature — emits the new DotSite from the SWITCH_SITE payload (no HTTP call needed) - DotToolbarComponent now uses globalStore.siteDetails as $currentSite (signal) instead of a one-shot getCurrentSite() observable - On SWITCH_SITE: call setCurrentSite() and navigate away from edit page - Remove stale ARCHIVE_SITE handler and DotcmsEventsService dependency (archive is already handled by DotSiteComponent directly) - Update toolbar spec: add SWITCH_SITE tests, remove dotcms-js imports Co-Authored-By: Claude Sonnet 4.6 --- .../dot-toolbar/dot-toolbar.component.ts | 36 ++---- .../dot-toolbar/dot-toolbar.spec.ts | 115 +++++++----------- .../with-websocket/with-websocket.feature.ts | 10 +- 3 files changed, 64 insertions(+), 97 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts index 7a01ebac9391..1103841a9e81 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts @@ -1,14 +1,13 @@ -import { Component, DestroyRef, OnInit, Signal, inject } from '@angular/core'; -import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { DividerModule } from 'primeng/divider'; import { ToolbarModule } from 'primeng/toolbar'; -import { map, switchMap, take } from 'rxjs/operators'; +import { switchMap, take } from 'rxjs/operators'; import { DotRouterService, DotSiteService } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotSite, FeaturedFlags } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotSiteComponent } from '@dotcms/ui'; @@ -39,32 +38,23 @@ import { DotCrumbtrailComponent } from '../dot-crumbtrail/dot-crumbtrail.compone export class DotToolbarComponent implements OnInit { #globalStore = inject(GlobalStore); readonly #dotRouterService = inject(DotRouterService); - readonly #dotcmsEventsService = inject(DotcmsEventsService); readonly #siteService = inject(DotSiteService); readonly #destroyRef = inject(DestroyRef); iframeOverlayService = inject(IframeOverlayService); featureFlagAnnouncements = FeaturedFlags.FEATURE_FLAG_ANNOUNCEMENTS; - $currentSite: Signal = toSignal(this.#siteService.getCurrentSite()); + $currentSite = this.#globalStore.siteDetails; ngOnInit(): void { - this.#dotcmsEventsService - .subscribeTo('ARCHIVE_SITE') - .pipe( - switchMap((data: DotSite) => - this.#siteService.getCurrentSite().pipe( - take(1), - map((currentSite: DotSite) => ({ data, currentSite })) - ) - ), - takeUntilDestroyed(this.#destroyRef) - ) - .subscribe(({ data, currentSite }) => { - if (data.hostname === currentSite.hostname && data.archived) { - this.#siteService.switchSite(null).subscribe((defaultSite: DotSite) => { - this.siteChange(defaultSite.identifier); - }); + // When another user/tab switches the site, update the store and navigate away from edit page + this.#globalStore + .switchSiteEvent$() + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe((site: DotSite) => { + this.#globalStore.setCurrentSite(site); + if (this.#dotRouterService.isEditPage()) { + this.#dotRouterService.goToSiteBrowser(); } }); } @@ -79,8 +69,6 @@ export class DotToolbarComponent implements OnInit { takeUntilDestroyed(this.#destroyRef) ) .subscribe((site: DotSite) => { - // wait for the site to be switched - // before redirecting to the site browser if (this.#dotRouterService.isEditPage()) { this.#dotRouterService.goToSiteBrowser(); } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts index b257b00ec089..0465314539cb 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { createComponentFactory, mockProvider, Spectator, SpyObject } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; -import { of } from 'rxjs'; +import { Subject, of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { Component, Injectable } from '@angular/core'; +import { Component, Injectable, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { ToolbarModule } from 'primeng/toolbar'; @@ -13,29 +13,15 @@ import { ToolbarModule } from 'primeng/toolbar'; import { DotCurrentUserService, DotEventsService, + DotEventsSocket, DotPropertiesService, DotRouterService, DotSiteService, DotSystemConfigService } from '@dotcms/data-access'; -import { - CoreWebService, - CoreWebServiceMock, - DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - SiteService, - StringUtils -} from '@dotcms/dotcms-js'; +import { DotSite } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; -import { - DotCurrentUserServiceMock, - MockDotRouterService, - mockSites, - SiteServiceMock -} from '@dotcms/utils-testing'; +import { DotCurrentUserServiceMock, MockDotRouterService, mockSites } from '@dotcms/utils-testing'; import { DotToolbarAnnouncementsComponent } from './components/dot-toolbar-announcements/dot-toolbar-announcements.component'; import { DotToolbarNotificationsComponent } from './components/dot-toolbar-notifications/dot-toolbar-notifications.component'; @@ -75,13 +61,6 @@ class MockToolbarNotificationsComponent {} }) class MockToolbarAnnouncementsComponent {} -export const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); -}; - describe('DotToolbarComponent', () => { let spectator: Spectator; let dotRouterService: SpyObject; @@ -90,8 +69,8 @@ describe('DotToolbarComponent', () => { let dotSiteService: SpyObject; let globalStore: SpyObject>; - const siteServiceMock = new SiteServiceMock(); const siteMock = mockSites[0]; + const switchSiteSubject = new Subject(); const createComponent = createComponentFactory({ component: DotToolbarComponent, @@ -111,7 +90,6 @@ describe('DotToolbarComponent', () => { }), mockProvider(DotSiteService, { getCurrentSite: jest.fn().mockReturnValue(of(siteMock)), - // switchSite API returns { hostSwitched: true }; toolbar then calls getCurrentSite() for the site switchSite: jest.fn().mockReturnValue(of({ hostSwitched: true })), getSites: jest.fn().mockReturnValue( of({ @@ -130,28 +108,20 @@ describe('DotToolbarComponent', () => { ) }), mockProvider(GlobalStore, { - setCurrentSite: jest.fn() + siteDetails: signal(siteMock), + setCurrentSite: jest.fn(), + switchSiteEvent$: jest.fn().mockReturnValue(switchSiteSubject.asObservable()) }), + mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), { provide: DotNavigationService, useClass: MockDotNavigationService }, - { provide: SiteService, useValue: siteServiceMock }, - mockProvider(ActivatedRoute, { - snapshot: { - _routerState: { - url: 'any/url' - } - } - }), - { provide: CoreWebService, useClass: CoreWebServiceMock }, + { + provide: ActivatedRoute, + useValue: { snapshot: { _routerState: { url: 'any/url' } } } + }, { provide: DotRouterService, useClass: MockDotRouterService }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotEventsService, - DotcmsEventsService, IframeOverlayService, DotNavLogoService, - DotEventsSocket, - DotcmsConfigService, - LoggerService, - StringUtils, { provide: DotSystemConfigService, useValue: { getSystemConfig: () => of({}) } @@ -174,45 +144,34 @@ describe('DotToolbarComponent', () => { jest.spyOn(spectator.component, 'siteChange'); jest.spyOn(iframeOverlayService, 'show'); jest.spyOn(iframeOverlayService, 'hide'); - // Reset feature flag mock to return true by default dotPropertiesService.getFeatureFlag.mockReturnValue(of(true)); }); it(`should has a dot-crumbtrail`, () => { spectator.detectChanges(); - - const crumbtrail = spectator.query('dot-crumbtrail'); - expect(crumbtrail).not.toBeNull(); + expect(spectator.query('dot-crumbtrail')).not.toBeNull(); }); it(`should has a dot-toolbar-notifications`, () => { spectator.detectChanges(); - - const dotToolbarNotifications = spectator.query('dot-toolbar-notifications'); - expect(dotToolbarNotifications).not.toBeNull(); + expect(spectator.query('dot-toolbar-notifications')).not.toBeNull(); }); it(`should has a dot-toolbar-user`, () => { spectator.detectChanges(); - - const dotToolbarUser = spectator.query('dot-toolbar-user'); - expect(dotToolbarUser).not.toBeNull(); + expect(spectator.query('dot-toolbar-user')).not.toBeNull(); }); it(`should has a dot-toolbar-announcements`, () => { dotPropertiesService.getFeatureFlag.mockReturnValue(of(true)); spectator.detectChanges(); - - const dotToolbarAnnouncements = spectator.query('dot-toolbar-announcements'); - expect(dotToolbarAnnouncements).not.toBeNull(); + expect(spectator.query('dot-toolbar-announcements')).not.toBeNull(); }); it(`should has not a dot-toolbar-announcements with feature flag disabled`, () => { dotPropertiesService.getFeatureFlag.mockReturnValue(of(false)); spectator.detectChanges(); - - const dotToolbarAnnouncements = spectator.query('dot-toolbar-announcements'); - expect(dotToolbarAnnouncements).toBeNull(); + expect(spectator.query('dot-toolbar-announcements')).toBeNull(); }); it(`should NOT go to site browser when site change in any portlet but edit page`, () => { @@ -227,13 +186,6 @@ describe('DotToolbarComponent', () => { }); it(`should go to site-browser when site change on edit page url`, () => { - Object.defineProperty(dotRouterService, 'currentPortlet', { - value: { - id: 'edit-page', - url: '' - }, - writable: true - }); jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); spectator.detectChanges(); spectator.triggerEventHandler('dot-site', 'onChange', siteMock.identifier); @@ -258,6 +210,29 @@ describe('DotToolbarComponent', () => { expect(globalStore.setCurrentSite).toHaveBeenCalledWith(siteMock); }); + describe('SWITCH_SITE WebSocket event', () => { + it('should update the store when SWITCH_SITE fires', () => { + spectator.detectChanges(); + const newSite = mockSites[1]; + switchSiteSubject.next(newSite); + expect(globalStore.setCurrentSite).toHaveBeenCalledWith(newSite); + }); + + it('should navigate to site browser when SWITCH_SITE fires on edit page', () => { + jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); + spectator.detectChanges(); + switchSiteSubject.next(mockSites[1]); + expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled(); + }); + + it('should NOT navigate to site browser when SWITCH_SITE fires on non-edit page', () => { + jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(false); + spectator.detectChanges(); + switchSiteSubject.next(mockSites[1]); + expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled(); + }); + }); + describe('dot-site component integration', () => { it(`should render dot-site component with correct inputs and bindings`, () => { spectator.detectChanges(); @@ -265,22 +240,18 @@ describe('DotToolbarComponent', () => { expect(siteComponent).not.toBeNull(); expect(siteComponent).toHaveClass('w-64'); - // Verify that value is bound to current site identifier - const componentInstance = spectator.component; - expect(componentInstance.$currentSite()).toEqual(siteMock); + expect(spectator.component.$currentSite()).toEqual(siteMock); }); it(`should call iframeOverlayService.show() when dot-site onShow event is triggered`, () => { spectator.detectChanges(); spectator.triggerEventHandler('dot-site', 'onShow', null); - expect(iframeOverlayService.show).toHaveBeenCalled(); }); it(`should call iframeOverlayService.hide() when dot-site onHide event is triggered`, () => { spectator.detectChanges(); spectator.triggerEventHandler('dot-site', 'onHide', null); - expect(iframeOverlayService.hide).toHaveBeenCalled(); }); }); diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts index 928419b671d6..8f16b358a9e3 100644 --- a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts @@ -7,6 +7,7 @@ import { inject } from '@angular/core'; import { switchMap, tap } from 'rxjs/operators'; import { DotEventsSocket, WebSocketStatus } from '@dotcms/data-access'; +import { DotSite } from '@dotcms/dotcms-models'; export interface WebSocketState { wsStatus: WebSocketStatus; @@ -60,7 +61,14 @@ export function withWebSocket() { eventsSocket.on('ARCHIVE_SITE'), eventsSocket.on('UN_ARCHIVE_SITE'), eventsSocket.on('DELETE_SITE') - ) + ), + + /** + * Observable that emits the new site when another user/tab switches + * the current site (SWITCH_SITE event). The payload contains the full + * DotSite object — no extra HTTP call needed. + */ + switchSiteEvent$: (): Observable => eventsSocket.on('SWITCH_SITE') })), withHooks({ onInit(store) { From 58eb937cd1b9b64e0bfb40ebbfaa52f94ae8d8a6 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 17:41:38 -0600 Subject: [PATCH 10/36] refactor(global-store): move site switching logic into GlobalStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The toolbar was acting as a mediator — calling DotSiteService directly and then writing back to the store it was reading from. Move that responsibility where it belongs: - Add switchCurrentSite(identifier) to GlobalStore: calls switchSite() → getCurrentSite() → patchState internally - Handle SWITCH_SITE WebSocket event inside store onInit instead of the toolbar, removing the need for setCurrentSite() entirely - DotToolbarComponent now delegates to globalStore.switchCurrentSite() and only owns the navigation side effect (goToSiteBrowser) Co-Authored-By: Claude Sonnet 4.6 --- .../dot-toolbar/dot-toolbar.component.ts | 31 +++----- .../dot-toolbar/dot-toolbar.spec.ts | 79 +++++-------------- .../dot-edit-content-form.component.spec.ts | 1 - .../libs/global-store/src/lib/store.spec.ts | 65 ++++++++++++--- core-web/libs/global-store/src/lib/store.ts | 44 +++++------ 5 files changed, 99 insertions(+), 121 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts index 1103841a9e81..eb44166ba939 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts @@ -5,10 +5,8 @@ import { FormsModule } from '@angular/forms'; import { DividerModule } from 'primeng/divider'; import { ToolbarModule } from 'primeng/toolbar'; -import { switchMap, take } from 'rxjs/operators'; - -import { DotRouterService, DotSiteService } from '@dotcms/data-access'; -import { DotSite, FeaturedFlags } from '@dotcms/dotcms-models'; +import { DotRouterService } from '@dotcms/data-access'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotSiteComponent } from '@dotcms/ui'; @@ -36,9 +34,8 @@ import { DotCrumbtrailComponent } from '../dot-crumbtrail/dot-crumbtrail.compone ] }) export class DotToolbarComponent implements OnInit { - #globalStore = inject(GlobalStore); + readonly #globalStore = inject(GlobalStore); readonly #dotRouterService = inject(DotRouterService); - readonly #siteService = inject(DotSiteService); readonly #destroyRef = inject(DestroyRef); iframeOverlayService = inject(IframeOverlayService); @@ -47,12 +44,11 @@ export class DotToolbarComponent implements OnInit { $currentSite = this.#globalStore.siteDetails; ngOnInit(): void { - // When another user/tab switches the site, update the store and navigate away from edit page + // Navigate away from edit page when another user/tab switches the site this.#globalStore .switchSiteEvent$() .pipe(takeUntilDestroyed(this.#destroyRef)) - .subscribe((site: DotSite) => { - this.#globalStore.setCurrentSite(site); + .subscribe(() => { if (this.#dotRouterService.isEditPage()) { this.#dotRouterService.goToSiteBrowser(); } @@ -61,19 +57,10 @@ export class DotToolbarComponent implements OnInit { siteChange(identifier: string | null): void { if (identifier) { - this.#siteService - .switchSite(identifier) - .pipe( - switchMap(() => this.#siteService.getCurrentSite()), - take(1), - takeUntilDestroyed(this.#destroyRef) - ) - .subscribe((site: DotSite) => { - if (this.#dotRouterService.isEditPage()) { - this.#dotRouterService.goToSiteBrowser(); - } - this.#globalStore.setCurrentSite(site); - }); + this.#globalStore.switchCurrentSite(identifier); + if (this.#dotRouterService.isEditPage()) { + this.#dotRouterService.goToSiteBrowser(); + } } } } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts index 0465314539cb..c673e2e71b35 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts @@ -16,7 +16,6 @@ import { DotEventsSocket, DotPropertiesService, DotRouterService, - DotSiteService, DotSystemConfigService } from '@dotcms/data-access'; import { DotSite } from '@dotcms/dotcms-models'; @@ -66,7 +65,6 @@ describe('DotToolbarComponent', () => { let dotRouterService: SpyObject; let dotPropertiesService: SpyObject; let iframeOverlayService: IframeOverlayService; - let dotSiteService: SpyObject; let globalStore: SpyObject>; const siteMock = mockSites[0]; @@ -88,28 +86,9 @@ describe('DotToolbarComponent', () => { mockProvider(DotPropertiesService, { getFeatureFlag: jest.fn().mockImplementation(() => of(true)) }), - mockProvider(DotSiteService, { - getCurrentSite: jest.fn().mockReturnValue(of(siteMock)), - switchSite: jest.fn().mockReturnValue(of({ hostSwitched: true })), - getSites: jest.fn().mockReturnValue( - of({ - sites: mockSites, - pagination: { - currentPage: 1, - perPage: 10, - totalRecords: mockSites.length - } - }) - ), - getSiteById: jest - .fn() - .mockImplementation((id: string) => - of(mockSites.find((s) => s.identifier === id) || siteMock) - ) - }), mockProvider(GlobalStore, { siteDetails: signal(siteMock), - setCurrentSite: jest.fn(), + switchCurrentSite: jest.fn(), switchSiteEvent$: jest.fn().mockReturnValue(switchSiteSubject.asObservable()) }), mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), @@ -139,7 +118,6 @@ describe('DotToolbarComponent', () => { dotRouterService = spectator.inject(DotRouterService); dotPropertiesService = spectator.inject(DotPropertiesService); iframeOverlayService = spectator.inject(IframeOverlayService); - dotSiteService = spectator.inject(DotSiteService); globalStore = spectator.inject(GlobalStore); jest.spyOn(spectator.component, 'siteChange'); jest.spyOn(iframeOverlayService, 'show'); @@ -174,50 +152,29 @@ describe('DotToolbarComponent', () => { expect(spectator.query('dot-toolbar-announcements')).toBeNull(); }); - it(`should NOT go to site browser when site change in any portlet but edit page`, () => { - jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(false); - spectator.detectChanges(); - spectator.triggerEventHandler('dot-site', 'onChange', siteMock.identifier); - expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled(); - expect(spectator.component.siteChange).toHaveBeenCalledWith(siteMock.identifier); - expect(dotSiteService.switchSite).toHaveBeenCalledWith(siteMock.identifier); - expect(dotSiteService.getCurrentSite).toHaveBeenCalled(); - expect(globalStore.setCurrentSite).toHaveBeenCalledWith(siteMock); - }); - - it(`should go to site-browser when site change on edit page url`, () => { - jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); - spectator.detectChanges(); - spectator.triggerEventHandler('dot-site', 'onChange', siteMock.identifier); - - expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled(); - expect(spectator.component.siteChange).toHaveBeenCalledWith(siteMock.identifier); - expect(dotSiteService.switchSite).toHaveBeenCalledWith(siteMock.identifier); - expect(dotSiteService.getCurrentSite).toHaveBeenCalled(); - expect(globalStore.setCurrentSite).toHaveBeenCalledWith(siteMock); - }); + describe('siteChange()', () => { + it(`should call switchCurrentSite and NOT navigate when not on edit page`, () => { + jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(false); + spectator.detectChanges(); + spectator.triggerEventHandler('dot-site', 'onChange', siteMock.identifier); - it(`should call switchSite then getCurrentSite and setCurrentSite when site changes`, () => { - spectator.detectChanges(); - dotSiteService.switchSite.mockClear(); - dotSiteService.getCurrentSite.mockClear(); - (globalStore.setCurrentSite as jest.Mock).mockClear(); + expect(spectator.component.siteChange).toHaveBeenCalledWith(siteMock.identifier); + expect(globalStore.switchCurrentSite).toHaveBeenCalledWith(siteMock.identifier); + expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled(); + }); - spectator.triggerEventHandler('dot-site', 'onChange', siteMock.identifier); + it(`should call switchCurrentSite and navigate when on edit page`, () => { + jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); + spectator.detectChanges(); + spectator.triggerEventHandler('dot-site', 'onChange', siteMock.identifier); - expect(dotSiteService.switchSite).toHaveBeenCalledWith(siteMock.identifier); - expect(dotSiteService.getCurrentSite).toHaveBeenCalled(); - expect(globalStore.setCurrentSite).toHaveBeenCalledWith(siteMock); + expect(spectator.component.siteChange).toHaveBeenCalledWith(siteMock.identifier); + expect(globalStore.switchCurrentSite).toHaveBeenCalledWith(siteMock.identifier); + expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled(); + }); }); describe('SWITCH_SITE WebSocket event', () => { - it('should update the store when SWITCH_SITE fires', () => { - spectator.detectChanges(); - const newSite = mockSites[1]; - switchSiteSubject.next(newSite); - expect(globalStore.setCurrentSite).toHaveBeenCalledWith(newSite); - }); - it('should navigate to site browser when SWITCH_SITE fires on edit page', () => { jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); spectator.detectChanges(); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts index 369d70875b55..fa94b3fbf5e2 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts @@ -101,7 +101,6 @@ describe('DotFormComponent', () => { mockProvider(DotVersionableService), mockProvider(GlobalStore, { loadCurrentSite: jest.fn(), - setCurrentSite: jest.fn(), siteDetails: jest.fn().mockReturnValue(null), addNewBreadcrumb: jest.fn() }), diff --git a/core-web/libs/global-store/src/lib/store.spec.ts b/core-web/libs/global-store/src/lib/store.spec.ts index 5620205f03b1..36ec3bf7a2eb 100644 --- a/core-web/libs/global-store/src/lib/store.spec.ts +++ b/core-web/libs/global-store/src/lib/store.spec.ts @@ -7,6 +7,7 @@ import { DotSiteService, DotSystemConfigService } from '@dotcms/data-access'; +import { DotSite } from '@dotcms/dotcms-models'; import { GlobalStore } from './store'; import { mockSiteEntity } from './store.mock'; @@ -14,24 +15,37 @@ import { mockSiteEntity } from './store.mock'; describe('GlobalStore', () => { let spectator: SpectatorService>; let store: InstanceType; + let switchSiteSubject: Subject; const createService = createServiceFactory({ service: GlobalStore, providers: [ mockProvider(DotCurrentUserService), - mockProvider(DotSiteService), + mockProvider(DotSiteService, { + getCurrentSite: jest.fn().mockReturnValue(of(null)), + switchSite: jest.fn().mockReturnValue(of({})) + }), mockProvider(DotSystemConfigService), mockProvider(DotEventsSocket, { connect: () => of({}), status$: () => new Subject(), - on: () => new Subject() + on: jest.fn().mockReturnValue(new Subject()) }) ] }); beforeEach(() => { + switchSiteSubject = new Subject(); spectator = createService(); store = spectator.service; + + // Wire up switchSiteSubject for SWITCH_SITE tests + const eventsSocket = spectator.inject(DotEventsSocket); + (eventsSocket.on as jest.Mock).mockImplementation((event: string) => { + if (event === 'SWITCH_SITE') return switchSiteSubject.asObservable(); + + return new Subject(); + }); }); describe('Initial State', () => { @@ -41,25 +55,50 @@ describe('GlobalStore', () => { }); }); - describe('Site Management', () => { - it('should call DotSiteService.getCurrentSite when loadCurrentSite is invoked', () => { - const mockService = spectator.inject(DotSiteService); - mockService.getCurrentSite.mockReturnValue(of(mockSiteEntity)); + describe('loadCurrentSite()', () => { + it('should call DotSiteService.getCurrentSite and update state', () => { + const siteService = spectator.inject(DotSiteService); + siteService.getCurrentSite.mockReturnValue(of(mockSiteEntity)); store.loadCurrentSite(); - expect(mockService.getCurrentSite).toHaveBeenCalled(); + expect(siteService.getCurrentSite).toHaveBeenCalled(); + expect(store.siteDetails()).toEqual(mockSiteEntity); + expect(store.currentSiteId()).toBe(mockSiteEntity.identifier); }); + }); - it('should properly update store state through setCurrentSite (verifies rxMethod target behavior)', () => { - // Initially store should be empty - expect(store.siteDetails()).toBeNull(); - expect(store.currentSiteId()).toBeNull(); + describe('switchCurrentSite()', () => { + it('should call switchSite then getCurrentSite and update siteDetails', () => { + const siteService = spectator.inject(DotSiteService); + const newSite: DotSite = { ...mockSiteEntity, identifier: 'new-site' }; + siteService.switchSite.mockReturnValue(of({})); + siteService.getCurrentSite.mockReturnValue(of(newSite)); - store.setCurrentSite(mockSiteEntity); + store.switchCurrentSite('new-site'); + + expect(siteService.switchSite).toHaveBeenCalledWith('new-site'); + expect(siteService.getCurrentSite).toHaveBeenCalled(); + expect(store.siteDetails()).toEqual(newSite); + }); + }); + + describe('SWITCH_SITE WebSocket event', () => { + it('should update siteDetails when SWITCH_SITE event fires', () => { + // Re-create service after wiring up the mock so onInit picks up the subject + spectator = createService(); + store = spectator.service; + const eventsSocket = spectator.inject(DotEventsSocket); + (eventsSocket.on as jest.Mock).mockImplementation((event: string) => { + if (event === 'SWITCH_SITE') return switchSiteSubject.asObservable(); + + return new Subject(); + }); + + // Trigger the event + switchSiteSubject.next(mockSiteEntity); expect(store.siteDetails()).toEqual(mockSiteEntity); - expect(store.currentSiteId()).toBe(mockSiteEntity.identifier); }); }); }); diff --git a/core-web/libs/global-store/src/lib/store.ts b/core-web/libs/global-store/src/lib/store.ts index 7307297eaec0..117f9d5fc1ba 100644 --- a/core-web/libs/global-store/src/lib/store.ts +++ b/core-web/libs/global-store/src/lib/store.ts @@ -13,7 +13,7 @@ import { pipe } from 'rxjs'; import { computed, inject } from '@angular/core'; -import { switchMap } from 'rxjs/operators'; +import { switchMap, tap } from 'rxjs/operators'; import { DotSiteService } from '@dotcms/data-access'; import { DotSite } from '@dotcms/dotcms-models'; @@ -128,9 +128,7 @@ export const GlobalStore = signalStore( siteService.getCurrentSite().pipe( tapResponse({ next: (siteDetails) => { - patchState(store, { - siteDetails - }); + patchState(store, { siteDetails }); }, error: (error) => { // TODO: Define a better error handling strategy for global store @@ -143,36 +141,34 @@ export const GlobalStore = signalStore( ), /** - * Sets the current site in the global store. - * - * This method updates the siteDetails property in the global state - * with the provided DotSite. - * - * @param site - The DotSite to set as the current site + * Switches the active site and updates the global store. * + * Calls DotSiteService.switchSite(), then fetches the now-current site + * and stores it. Use this when the user explicitly picks a site in the UI. */ - setCurrentSite: (site: DotSite) => { - patchState(store, { - siteDetails: site - }); - } + switchCurrentSite: rxMethod( + pipe( + switchMap((identifier) => + siteService.switchSite(identifier).pipe( + switchMap(() => siteService.getCurrentSite()), + tap((siteDetails) => patchState(store, { siteDetails })) + ) + ) + ) + ) }; }), withUser(), withMenu(), withFeature(({ menuItemsEntities }) => withBreadcrumbs(menuItemsEntities)), withHooks({ - /** - * Automatically loads the current site when the store is initialized. - * - * The system configuration is automatically loaded by the withSystem feature. - * This ensures the currentSiteId is available immediately after injecting - * the store in any component. - */ onInit(store) { - // Load current site on store initialization - // System configuration is automatically loaded by withSystem feature store.loadCurrentSite(); + // Keep siteDetails in sync when another user/tab switches the site + store + .switchSiteEvent$() + .pipe(tap((site) => patchState(store, { siteDetails: site }))) + .subscribe(); } }) ); From 830702755a3162b0f7b3511861f971a59cd0817c Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 17:59:30 -0600 Subject: [PATCH 11/36] refactor(toolbar): extract SWITCH_SITE navigation into DotSiteNavigationEffect Navigation away from the edit page on site switch is an app-level concern, not a toolbar concern. Move it into a dedicated effect service that lives for the full application lifetime: - Add DotSiteNavigationEffect: subscribes to switchSiteEvent$() and calls goToSiteBrowser() when on edit page - Register it eagerly via providers.ts (providedIn root + listed) - DotToolbarComponent is now a pure UI component with no lifecycle hooks - Add DotSiteNavigationEffect.spec.ts covering both navigation paths Co-Authored-By: Claude Sonnet 4.6 --- .../dot-site-navigation.effect.spec.ts | 54 +++++++++++++++++++ .../dot-site-navigation.effect.ts | 28 ++++++++++ core-web/apps/dotcms-ui/src/app/providers.ts | 4 +- .../dot-toolbar/dot-toolbar.component.ts | 18 +------ .../dot-toolbar/dot-toolbar.spec.ts | 22 ++------ 5 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts create mode 100644 core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.ts diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts new file mode 100644 index 000000000000..6921fb0f7401 --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts @@ -0,0 +1,54 @@ +import { + createServiceFactory, + mockProvider, + SpectatorService, + SpyObject +} from '@ngneat/spectator/jest'; +import { Subject } from 'rxjs'; + +import { DotRouterService } from '@dotcms/data-access'; +import { DotSite } from '@dotcms/dotcms-models'; +import { GlobalStore } from '@dotcms/store'; +import { MockDotRouterService, mockSites } from '@dotcms/utils-testing'; + +import { DotSiteNavigationEffect } from './dot-site-navigation.effect'; + +describe('DotSiteNavigationEffect', () => { + let spectator: SpectatorService; + let dotRouterService: SpyObject; + let switchSiteSubject: Subject; + + const createService = createServiceFactory({ + service: DotSiteNavigationEffect, + providers: [ + { provide: DotRouterService, useClass: MockDotRouterService }, + mockProvider(GlobalStore, { + switchSiteEvent$: jest.fn() + }) + ] + }); + + beforeEach(() => { + switchSiteSubject = new Subject(); + + spectator = createService(); + dotRouterService = spectator.inject(DotRouterService); + + const globalStore = spectator.inject(GlobalStore); + (globalStore.switchSiteEvent$ as jest.Mock).mockReturnValue( + switchSiteSubject.asObservable() + ); + }); + + it('should navigate to site browser when SWITCH_SITE fires on edit page', () => { + jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); + switchSiteSubject.next(mockSites[0]); + expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled(); + }); + + it('should NOT navigate when SWITCH_SITE fires on a non-edit page', () => { + jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(false); + switchSiteSubject.next(mockSites[0]); + expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled(); + }); +}); diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.ts new file mode 100644 index 000000000000..505c11ad9079 --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.ts @@ -0,0 +1,28 @@ +import { Injectable, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { DotRouterService } from '@dotcms/data-access'; +import { GlobalStore } from '@dotcms/store'; + +/** + * App-level effect that navigates away from the edit page whenever + * another user/tab switches the current site via WebSocket. + * + * Provided eagerly in app.config.ts so it is active for the full + * application lifetime without being tied to any particular component. + */ +@Injectable({ providedIn: 'root' }) +export class DotSiteNavigationEffect { + readonly #dotRouterService = inject(DotRouterService); + + constructor() { + inject(GlobalStore) + .switchSiteEvent$() + .pipe(takeUntilDestroyed()) + .subscribe(() => { + if (this.#dotRouterService.isEditPage()) { + this.#dotRouterService.goToSiteBrowser(); + } + }); + } +} diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index d5acd01a975e..fa2fd4f804ec 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -52,6 +52,7 @@ import { DotAccountService } from './api/services/dot-account-service'; import { DotDownloadBundleDialogService } from './api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from './api/services/dot-menu.service'; import { DotParseHtmlService } from './api/services/dot-parse-html/dot-parse-html.service'; +import { DotSiteNavigationEffect } from './api/services/dot-site-navigation/dot-site-navigation.effect'; import { AuthGuardService } from './api/services/guards/auth-guard.service'; import { ContentletGuardService } from './api/services/guards/contentlet-guard.service'; import { DefaultGuardService } from './api/services/guards/default-guard.service'; @@ -138,7 +139,8 @@ const PROVIDERS: Provider[] = [ useClass: DotTitleStrategy }, GlobalStore, - DotSystemConfigService + DotSystemConfigService, + DotSiteNavigationEffect ]; export const ENV_PROVIDERS = [...PROVIDERS]; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts index eb44166ba939..96c6106bcb17 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts @@ -1,5 +1,4 @@ -import { Component, DestroyRef, OnInit, inject } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Component, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { DividerModule } from 'primeng/divider'; @@ -33,28 +32,15 @@ import { DotCrumbtrailComponent } from '../dot-crumbtrail/dot-crumbtrail.compone FormsModule ] }) -export class DotToolbarComponent implements OnInit { +export class DotToolbarComponent { readonly #globalStore = inject(GlobalStore); readonly #dotRouterService = inject(DotRouterService); - readonly #destroyRef = inject(DestroyRef); iframeOverlayService = inject(IframeOverlayService); featureFlagAnnouncements = FeaturedFlags.FEATURE_FLAG_ANNOUNCEMENTS; $currentSite = this.#globalStore.siteDetails; - ngOnInit(): void { - // Navigate away from edit page when another user/tab switches the site - this.#globalStore - .switchSiteEvent$() - .pipe(takeUntilDestroyed(this.#destroyRef)) - .subscribe(() => { - if (this.#dotRouterService.isEditPage()) { - this.#dotRouterService.goToSiteBrowser(); - } - }); - } - siteChange(identifier: string | null): void { if (identifier) { this.#globalStore.switchCurrentSite(identifier); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts index c673e2e71b35..6fae2aaa57e6 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts @@ -18,7 +18,6 @@ import { DotRouterService, DotSystemConfigService } from '@dotcms/data-access'; -import { DotSite } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotCurrentUserServiceMock, MockDotRouterService, mockSites } from '@dotcms/utils-testing'; @@ -28,6 +27,7 @@ import { DotToolbarUserComponent } from './components/dot-toolbar-user/dot-toolb import { DotToolbarComponent } from './dot-toolbar.component'; import { DotNavLogoService } from '../../../api/services/dot-nav-logo/dot-nav-logo.service'; +import { DotSiteNavigationEffect } from '../../../api/services/dot-site-navigation/dot-site-navigation.effect'; import { DotShowHideFeatureDirective } from '../../../shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive'; import { IframeOverlayService } from '../_common/iframe/service/iframe-overlay.service'; import { DotCrumbtrailComponent } from '../dot-crumbtrail/dot-crumbtrail.component'; @@ -68,7 +68,6 @@ describe('DotToolbarComponent', () => { let globalStore: SpyObject>; const siteMock = mockSites[0]; - const switchSiteSubject = new Subject(); const createComponent = createComponentFactory({ component: DotToolbarComponent, @@ -89,8 +88,9 @@ describe('DotToolbarComponent', () => { mockProvider(GlobalStore, { siteDetails: signal(siteMock), switchCurrentSite: jest.fn(), - switchSiteEvent$: jest.fn().mockReturnValue(switchSiteSubject.asObservable()) + switchSiteEvent$: jest.fn().mockReturnValue(new Subject()) }), + mockProvider(DotSiteNavigationEffect), mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(new Subject()) }), { provide: DotNavigationService, useClass: MockDotNavigationService }, { @@ -174,22 +174,6 @@ describe('DotToolbarComponent', () => { }); }); - describe('SWITCH_SITE WebSocket event', () => { - it('should navigate to site browser when SWITCH_SITE fires on edit page', () => { - jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); - spectator.detectChanges(); - switchSiteSubject.next(mockSites[1]); - expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled(); - }); - - it('should NOT navigate to site browser when SWITCH_SITE fires on non-edit page', () => { - jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(false); - spectator.detectChanges(); - switchSiteSubject.next(mockSites[1]); - expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled(); - }); - }); - describe('dot-site component integration', () => { it(`should render dot-site component with correct inputs and bindings`, () => { spectator.detectChanges(); From 55598645149473b2eeac6a4870c6aec987ed3150 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 19:07:01 -0600 Subject: [PATCH 12/36] refactor(toolbar): remove redundant navigation logic handled by DotSiteNavigationEffect DotSiteNavigationEffect already handles goToSiteBrowser() when a SWITCH_SITE WebSocket event fires, which includes user-initiated switches from the toolbar. Removing the duplicated check keeps navigation responsibility in one place. Co-Authored-By: Claude Sonnet 4.6 --- .../app/view/components/dot-toolbar/dot-toolbar.component.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts index 96c6106bcb17..d953607ff3d0 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts @@ -4,7 +4,6 @@ import { FormsModule } from '@angular/forms'; import { DividerModule } from 'primeng/divider'; import { ToolbarModule } from 'primeng/toolbar'; -import { DotRouterService } from '@dotcms/data-access'; import { FeaturedFlags } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotSiteComponent } from '@dotcms/ui'; @@ -34,7 +33,6 @@ import { DotCrumbtrailComponent } from '../dot-crumbtrail/dot-crumbtrail.compone }) export class DotToolbarComponent { readonly #globalStore = inject(GlobalStore); - readonly #dotRouterService = inject(DotRouterService); iframeOverlayService = inject(IframeOverlayService); featureFlagAnnouncements = FeaturedFlags.FEATURE_FLAG_ANNOUNCEMENTS; @@ -44,9 +42,6 @@ export class DotToolbarComponent { siteChange(identifier: string | null): void { if (identifier) { this.#globalStore.switchCurrentSite(identifier); - if (this.#dotRouterService.isEditPage()) { - this.#dotRouterService.goToSiteBrowser(); - } } } } From 5d530fa39b3c965ddfb0bfe2cd22820d0d48a0b6 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 19:30:57 -0600 Subject: [PATCH 13/36] refactor(site-switch): replace legacy SiteService.switchSite$ with GlobalStore.switchSiteEvent$ Replace all usages of the legacy dotcms-js SiteService.switchSite$ observable in apps/dotcms-ui with GlobalStore.switchSiteEvent$, which is backed by the WebSocket SWITCH_SITE event. This consolidates site-switch reactivity through the global store. Co-Authored-By: Claude Sonnet 4.6 --- .../container-list.component.ts | 10 ++++----- .../dot-template-create-edit.component.ts | 11 +++++----- .../dot-template-list.component.ts | 15 ++++++++----- .../iframe-porlet-legacy.component.ts | 22 +++++++++---------- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts index 9819d0ce4fbf..c26d2c95ac13 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts @@ -25,7 +25,6 @@ import { DotMessageService, DotSiteBrowserService } from '@dotcms/data-access'; -import { SiteService } from '@dotcms/dotcms-js'; import { CONTAINER_SOURCE, DotActionBulkResult, @@ -36,6 +35,7 @@ import { DotMessageSeverity, DotMessageType } from '@dotcms/dotcms-models'; +import { GlobalStore } from '@dotcms/store'; import { DotActionMenuButtonComponent, DotAddToBundleComponent, @@ -84,7 +84,7 @@ export class ContainerListComponent implements OnDestroy { private dotMessageService = inject(DotMessageService); private dotMessageDisplayService = inject(DotMessageDisplayService); private dialogService = inject(DialogService); - private siteService = inject(SiteService); + readonly #globalStore = inject(GlobalStore); @ViewChild('actionsMenu') actionsMenu: Menu; @@ -106,9 +106,9 @@ export class ContainerListComponent implements OnDestroy { this.selectedContainers = []; }); - this.siteService.switchSite$.subscribe(({ identifier }) => - this.#store.getContainersByHost(identifier) - ); + this.#globalStore + .switchSiteEvent$() + .subscribe(({ identifier }) => this.#store.getContainersByHost(identifier)); } ngOnDestroy(): void { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts index 6161c822819a..1f531156698e 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts @@ -15,7 +15,6 @@ import { DialogService, DynamicDialogModule } from 'primeng/dynamicdialog'; import { takeUntil, tap } from 'rxjs/operators'; import { DotMessageService } from '@dotcms/data-access'; -import { SiteService } from '@dotcms/dotcms-js'; import { DotLayout, DotTemplate } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotApiLinkComponent, DotMessagePipe } from '@dotcms/ui'; @@ -49,7 +48,6 @@ export class DotTemplateCreateEditComponent implements OnInit, OnDestroy { private fb = inject(UntypedFormBuilder); private dialogService = inject(DialogService); private dotMessageService = inject(DotMessageService); - private dotSiteService = inject(SiteService); readonly #store = inject(DotTemplateStore); readonly #globalStore = inject(GlobalStore); @@ -246,9 +244,12 @@ export class DotTemplateCreateEditComponent implements OnInit, OnDestroy { } private setSwitchSiteListener(): void { - this.dotSiteService.switchSite$.pipe(takeUntil(this.destroy$)).subscribe(() => { - this.#store.goToTemplateList(); - }); + this.#globalStore + .switchSiteEvent$() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.#store.goToTemplateList(); + }); } private formatTemplateItem({ layout, body, themeId }: DotTemplate): DotTemplateItem { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.ts index c647081436f7..b9fa5d51d071 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.ts @@ -34,7 +34,7 @@ import { DotSiteBrowserService, PushPublishService } from '@dotcms/data-access'; -import { DotPushPublishDialogService, SiteService } from '@dotcms/dotcms-js'; +import { DotPushPublishDialogService } from '@dotcms/dotcms-js'; import { DotActionBulkResult, DotActionMenuItem, @@ -44,6 +44,7 @@ import { DotMessageType, DotTemplate } from '@dotcms/dotcms-models'; +import { GlobalStore } from '@dotcms/store'; import { DotAddToBundleComponent, DotContentletStatusChipComponent, @@ -107,7 +108,7 @@ export class DotTemplateListComponent implements OnInit { private dotMessageService = inject(DotMessageService); private dotPushPublishDialogService = inject(DotPushPublishDialogService); private dotRouterService = inject(DotRouterService); - private dotSiteService = inject(SiteService); + readonly #globalStore = inject(GlobalStore); private dotTemplatesService = inject(DotTemplatesService); private pushPublishService = inject(PushPublishService); private destroyRef = inject(DestroyRef); @@ -170,10 +171,12 @@ export class DotTemplateListComponent implements OnInit { // Load initial templates this.loadTemplates(); - // Listen for site changes using SiteService - this.dotSiteService.switchSite$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { - this.dotRouterService.gotoPortlet('templates'); - }); + this.#globalStore + .switchSiteEvent$() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.dotRouterService.gotoPortlet('templates'); + }); } /** diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts index 96e01092b9b2..274f7d9756e4 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts @@ -7,8 +7,9 @@ import { ActivatedRoute, RouterModule, UrlSegment } from '@angular/router'; import { map, mergeMap, pluck, takeUntil, withLatestFrom } from 'rxjs/operators'; import { DotContentTypeService, DotIframeService, DotRouterService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoggerService, SiteService } from '@dotcms/dotcms-js'; +import { DotcmsEventsService, LoggerService } from '@dotcms/dotcms-js'; import { UI_STORAGE_KEY } from '@dotcms/dotcms-models'; +import { GlobalStore } from '@dotcms/store'; import { DotNotLicenseComponent } from '@dotcms/ui'; import { DotLoadingIndicatorService } from '@dotcms/utils'; @@ -30,7 +31,7 @@ export class IframePortletLegacyComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private dotCustomEventHandlerService = inject(DotCustomEventHandlerService); loggerService = inject(LoggerService); - siteService = inject(SiteService); + readonly #globalStore = inject(GlobalStore); private dotcmsEventsService = inject(DotcmsEventsService); private dotIframeService = inject(DotIframeService); @@ -46,15 +47,14 @@ export class IframePortletLegacyComponent implements OnInit, OnDestroy { this.reloadIframePortlet(portletId); } }); - /** - * skip first - to avoid subscription when page loads due login user subscription: - * https://github.com/dotCMS/core-web/blob/main/projects/dotcms-js/src/lib/core/site.service.ts#L58 - */ - this.siteService.switchSite$.pipe(takeUntil(this.destroy$)).subscribe(() => { - if (this.url.getValue() !== '') { - this.reloadIframePortlet(); - } - }); + this.#globalStore + .switchSiteEvent$() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + if (this.url.getValue() !== '') { + this.reloadIframePortlet(); + } + }); this.route.data .pipe(pluck('canAccessPortlet'), takeUntil(this.destroy$)) From a64bb927f2aa6b842c51fb71a334d205ee2539c3 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Sun, 15 Mar 2026 21:16:23 -0600 Subject: [PATCH 14/36] refactor(websockets): replace DotcmsEventsService with DotEventsSocket Migrate all subscribeTo/subscribeToEvents call sites to the new DotEventsSocket.on() API. Also fixes a memory leak in DotToolbarNotificationsComponent by adding takeUntilDestroyed. Co-Authored-By: Claude Sonnet 4.6 --- .../iframe-component/iframe.component.spec.ts | 42 +++++++++++-------- .../iframe-component/iframe.component.ts | 37 +++++++++------- .../iframe-porlet-legacy.component.spec.ts | 9 ++-- .../iframe-porlet-legacy.component.ts | 15 ++++--- ...ot-large-message-display.component.spec.ts | 31 +++++++------- .../dot-large-message-display.component.ts | 8 ++-- .../dot-toolbar-notifications.component.ts | 10 +++-- .../dot-message-display.service.spec.ts | 21 ++++++---- .../dot-message-display.service.ts | 8 ++-- 9 files changed, 103 insertions(+), 78 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts index 1a3bebe38f70..c71700c952a1 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts @@ -1,19 +1,22 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { Subject } from 'rxjs'; + import { Component, DebugElement } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { DotIframeService, DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; +import { + DotEventsSocket, + DotIframeService, + DotRouterService, + DotUiColorsService +} from '@dotcms/data-access'; +import { LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { DotMessagePipe, DotSafeHtmlPipe } from '@dotcms/ui'; import { DotLoadingIndicatorService } from '@dotcms/utils'; -import { - DotcmsEventsServiceMock, - LoginServiceMock, - MockDotRouterService -} from '@dotcms/utils-testing'; +import { LoginServiceMock, MockDotRouterService } from '@dotcms/utils-testing'; import { IframeOverlayService } from './../service/iframe-overlay.service'; import { IframeComponent } from './iframe.component'; @@ -40,9 +43,18 @@ describe('IframeComponent', () => { let iframeEl: DebugElement; let dotIframeService: DotIframeService; let dotUiColorsService: DotUiColorsService; - const dotcmsEventsService = new DotcmsEventsServiceMock(); let dotRouterService: DotRouterService; + const eventSubjects: Record> = {}; + const mockDotEventsSocket = { + on: jest.fn((eventType: string) => { + if (!eventSubjects[eventType]) { + eventSubjects[eventType] = new Subject(); + } + return eventSubjects[eventType].asObservable(); + }) + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [MockDotLoadingIndicatorComponent], @@ -59,7 +71,7 @@ describe('IframeComponent', () => { IframeOverlayService, DotIframeService, { provide: LoginService, useClass: LoginServiceMock }, - { provide: DotcmsEventsService, useValue: dotcmsEventsService }, + { provide: DotEventsSocket, useValue: mockDotEventsSocket }, { provide: DotRouterService, useClass: MockDotRouterService }, { provide: DotUiColorsService, useClass: MockDotUiColorsService }, LoggerService, @@ -107,18 +119,14 @@ describe('IframeComponent', () => { }); it('should reload on DELETE_BUNDLE and on publishing-queue portlet websocket event', () => { - dotcmsEventsService.triggerSubscribeTo('DELETE_BUNDLE', { - name: 'DELETE_BUNDLE' - }); + eventSubjects['DELETE_BUNDLE'].next({ name: 'DELETE_BUNDLE' }); expect(comp.iframeElement.nativeElement.contentWindow.postMessage).toHaveBeenCalledWith( 'reload' ); }); it('should reload on PAGE_RELOAD websocket event', () => { - dotcmsEventsService.triggerSubscribeTo('PAGE_RELOAD', { - name: 'PAGE_RELOAD' - }); + eventSubjects['PAGE_RELOAD'].next({ name: 'PAGE_RELOAD' }); expect(comp.iframeElement.nativeElement.contentWindow.postMessage).toHaveBeenCalledWith( 'reload' ); @@ -300,9 +308,7 @@ describe('IframeComponent', () => { } } }; - dotcmsEventsService.triggerSubscribeTo('OSGI_BUNDLES_LOADED', { - name: 'OSGI_BUNDLES_LOADED' - }); + eventSubjects['OSGI_BUNDLES_LOADED'].next(undefined); tick(4500); expect(comp.iframeElement.nativeElement.contentWindow.getBundlesData).toHaveBeenCalledTimes( 1 diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts index b06c2888835e..aff4854d7b59 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts @@ -1,4 +1,4 @@ -import { Subject } from 'rxjs'; +import { merge, Subject } from 'rxjs'; import { ChangeDetectorRef, @@ -15,10 +15,15 @@ import { ViewChild } from '@angular/core'; -import { debounceTime, filter, takeUntil } from 'rxjs/operators'; +import { debounceTime, filter, map, takeUntil } from 'rxjs/operators'; -import { DotIframeService, DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { DotcmsEventsService, DotEventTypeWrapper, LoggerService } from '@dotcms/dotcms-js'; +import { + DotEventsSocket, + DotIframeService, + DotRouterService, + DotUiColorsService +} from '@dotcms/data-access'; +import { LoggerService } from '@dotcms/dotcms-js'; import { DotFunctionInfo } from '@dotcms/dotcms-models'; import { DotLoadingIndicatorService } from '@dotcms/utils'; @@ -46,7 +51,7 @@ export class IframeComponent implements OnInit, OnDestroy { private dotIframeService = inject(DotIframeService); private dotRouterService = inject(DotRouterService); private dotUiColorsService = inject(DotUiColorsService); - private dotcmsEventsService = inject(DotcmsEventsService); + private dotEventsSocket = inject(DotEventsSocket); private ngZone = inject(NgZone); private cdr = inject(ChangeDetectorRef); dotLoadingIndicatorService = inject(DotLoadingIndicatorService); @@ -177,23 +182,27 @@ export class IframeComponent implements OnInit, OnDestroy { 'PAGE_RELOAD' ]; - const webSocketEvents$ = this.dotcmsEventsService - .subscribeToEvents(events) - .pipe(takeUntil(this.destroy$)); + const webSocketEvents$ = merge( + ...events.map((eventType) => + this.dotEventsSocket + .on(eventType) + .pipe(map((data) => ({ data, name: eventType }))) + ) + ).pipe(takeUntil(this.destroy$)); webSocketEvents$ .pipe(filter(() => this.dotRouterService.currentPortlet.id === 'site-browser')) - .subscribe((event: DotEventTypeWrapper) => { + .subscribe((event) => { this.loggerService.debug('Capturing Site Browser event', event.name, event.data); }); webSocketEvents$ .pipe( filter( - (event: DotEventTypeWrapper) => + (event) => (this.iframeElement.nativeElement.contentWindow && event.name === 'DELETE_BUNDLE') || - event.name === 'PAGE_RELOAD' // Provinding this event so backend devs can reload the jsp easily + event.name === 'PAGE_RELOAD' // Providing this event so backend devs can reload the jsp easily ) ) .subscribe(() => { @@ -202,12 +211,12 @@ export class IframeComponent implements OnInit, OnDestroy { /** * The debouncetime is required because when the websocket event is received, - * the list of plugins still cannot be updated, thi is because the framework (OSGI) + * the list of plugins still cannot be updated, this is because the framework (OSGI) * needs to restart before the list can be refreshed. * Currently, an event cannot be emitted when the framework finishes restarting. */ - this.dotcmsEventsService - .subscribeTo('OSGI_BUNDLES_LOADED') + this.dotEventsSocket + .on('OSGI_BUNDLES_LOADED') .pipe(takeUntil(this.destroy$), debounceTime(4000)) .subscribe(() => { this.dotIframeService.run({ name: 'getBundlesData' }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts index 5719d3e6f8e7..b894e3acd7af 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { of } from 'rxjs'; +import { EMPTY, of } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { DebugElement } from '@angular/core'; @@ -16,6 +16,7 @@ import { DotContentTypeService, DotCurrentUserService, DotEventsService, + DotEventsSocket as DotEventsSocketDataAccess, DotFormatDateService, DotGlobalMessageService, DotHttpErrorManagerService, @@ -34,7 +35,6 @@ import { CoreWebService, DotcmsConfigService, DotcmsEventsService, - DotEventsSocket, DotEventsSocketURL, DotPushPublishDialogService, LoggerService, @@ -126,7 +126,10 @@ xdescribe('IframePortletLegacyComponent', () => { DotCurrentUserService, DotMessageDisplayService, DotcmsEventsService, - DotEventsSocket, + { + provide: DotEventsSocketDataAccess, + useValue: { on: jest.fn().mockReturnValue(EMPTY) } + }, { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, DotFormatDateService, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts index 274f7d9756e4..b66edd342c4f 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.ts @@ -6,8 +6,13 @@ import { ActivatedRoute, RouterModule, UrlSegment } from '@angular/router'; import { map, mergeMap, pluck, takeUntil, withLatestFrom } from 'rxjs/operators'; -import { DotContentTypeService, DotIframeService, DotRouterService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoggerService } from '@dotcms/dotcms-js'; +import { + DotContentTypeService, + DotEventsSocket, + DotIframeService, + DotRouterService +} from '@dotcms/data-access'; +import { LoggerService } from '@dotcms/dotcms-js'; import { UI_STORAGE_KEY } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotNotLicenseComponent } from '@dotcms/ui'; @@ -32,7 +37,7 @@ export class IframePortletLegacyComponent implements OnInit, OnDestroy { private dotCustomEventHandlerService = inject(DotCustomEventHandlerService); loggerService = inject(LoggerService); readonly #globalStore = inject(GlobalStore); - private dotcmsEventsService = inject(DotcmsEventsService); + private dotEventsSocket = inject(DotEventsSocket); private dotIframeService = inject(DotIframeService); canAccessPortlet: boolean; @@ -165,8 +170,8 @@ export class IframePortletLegacyComponent implements OnInit, OnDestroy { with the function refreshFakeJax defined in view_contentlets_js_inc.jsp. */ private subscribeToAIGeneration(): void { - this.dotcmsEventsService - .subscribeTo('AI_CONTENT_PROMPT') + this.dotEventsSocket + .on('AI_CONTENT_PROMPT') .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.dotIframeService.run({ name: 'refreshFakeJax' }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.spec.ts index 31ab50242ed2..52bd73b5f689 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.spec.ts @@ -1,9 +1,9 @@ import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { Subject } from 'rxjs'; import { fakeAsync, tick } from '@angular/core/testing'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; -import { DotcmsEventsServiceMock } from '@dotcms/utils-testing'; +import { DotEventsSocket } from '@dotcms/data-access'; import { DotLargeMessageDisplayComponent } from './dot-large-message-display.component'; @@ -11,29 +11,28 @@ import { DotParseHtmlService } from '../../../api/services/dot-parse-html/dot-pa describe('DotLargeMessageDisplayComponent', () => { let spectator: Spectator; - let dotcmsEventsServiceMock: DotcmsEventsServiceMock; + const largeMessageSubject = new Subject(); + const mockDotEventsSocket = { + on: jest.fn().mockReturnValue(largeMessageSubject.asObservable()) + }; const createComponent = createComponentFactory({ component: DotLargeMessageDisplayComponent, detectChanges: false, imports: [], providers: [ - { provide: DotcmsEventsService, useClass: DotcmsEventsServiceMock }, + { provide: DotEventsSocket, useValue: mockDotEventsSocket }, DotParseHtmlService ] }); beforeEach(() => { spectator = createComponent(); - dotcmsEventsServiceMock = spectator.inject( - DotcmsEventsService - ) as unknown as DotcmsEventsServiceMock; - jest.spyOn(dotcmsEventsServiceMock, 'subscribeTo'); }); it('should create DotLargeMessageDisplayComponent', fakeAsync(() => { spectator.fixture.detectChanges(false); // run ngOnInit so component subscribes - dotcmsEventsServiceMock.triggerSubscribeTo('LARGE_MESSAGE', { + largeMessageSubject.next({ title: 'title Test', height: '200', width: '1000', @@ -50,7 +49,7 @@ describe('DotLargeMessageDisplayComponent', () => { true ); expect(spectator.component.messages[0].code?.content).toBe('codeTest'); - expect(dotcmsEventsServiceMock.subscribeTo).toHaveBeenCalledTimes(1); + expect(mockDotEventsSocket.on).toHaveBeenCalledWith('LARGE_MESSAGE'); tick(0); spectator.fixture.detectChanges(false); @@ -59,7 +58,7 @@ describe('DotLargeMessageDisplayComponent', () => { it('should render script tag from body', fakeAsync(() => { spectator.fixture.detectChanges(false); - dotcmsEventsServiceMock.triggerSubscribeTo('LARGE_MESSAGE', { + largeMessageSubject.next({ title: 'title Test', body: '

Hello World

' }); @@ -74,7 +73,7 @@ describe('DotLargeMessageDisplayComponent', () => { it('should render script tag from script property', fakeAsync(() => { spectator.fixture.detectChanges(false); - dotcmsEventsServiceMock.triggerSubscribeTo('LARGE_MESSAGE', { + largeMessageSubject.next({ title: 'title Test', body: '

Hello World

', script: 'console.log("script from prop")' @@ -91,7 +90,7 @@ describe('DotLargeMessageDisplayComponent', () => { it('should remove dialog when it is close', fakeAsync(() => { spectator.fixture.detectChanges(false); - dotcmsEventsServiceMock.triggerSubscribeTo('LARGE_MESSAGE', { + largeMessageSubject.next({ title: 'title Test', body: '

Hello World

', script: 'console.log("script from prop")' @@ -109,7 +108,7 @@ describe('DotLargeMessageDisplayComponent', () => { it('should set default height and width', fakeAsync(() => { spectator.fixture.detectChanges(false); - dotcmsEventsServiceMock.triggerSubscribeTo('LARGE_MESSAGE', { + largeMessageSubject.next({ title: 'title Test', body: 'bodyTest', code: { lang: 'eng', content: 'codeTest' } @@ -126,7 +125,7 @@ describe('DotLargeMessageDisplayComponent', () => { it('should show two dialogs', fakeAsync(() => { spectator.fixture.detectChanges(false); - dotcmsEventsServiceMock.triggerSubscribeTo('LARGE_MESSAGE', { + largeMessageSubject.next({ title: 'title Test', body: 'bodyTest', code: { lang: 'eng', content: 'codeTest' } @@ -136,7 +135,7 @@ describe('DotLargeMessageDisplayComponent', () => { expect(spectator.component.messages.length).toBe(1); - dotcmsEventsServiceMock.triggerSubscribeTo('LARGE_MESSAGE', { + largeMessageSubject.next({ title: 'title Test 2', body: 'bodyTest 2', code: { lang: 'eng', content: 'codeTest 2' } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.ts index c04a4cd72db3..cc4d64237808 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-large-message-display/dot-large-message-display.component.ts @@ -14,7 +14,7 @@ import { DialogModule, Dialog } from 'primeng/dialog'; import { filter, takeUntil } from 'rxjs/operators'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; +import { DotEventsSocket } from '@dotcms/data-access'; import { DotParseHtmlService } from '../../../api/services/dot-parse-html/dot-parse-html.service'; @@ -38,7 +38,7 @@ interface DotLargeMessageDisplayParams { providers: [DotParseHtmlService] }) export class DotLargeMessageDisplayComponent implements OnInit, OnDestroy, AfterViewInit { - private dotcmsEventsService = inject(DotcmsEventsService); + private dotEventsSocket = inject(DotEventsSocket); private dotParseHtmlService = inject(DotParseHtmlService); @ViewChildren(Dialog) dialogs: QueryList; @@ -114,8 +114,8 @@ export class DotLargeMessageDisplayComponent implements OnInit, OnDestroy, After } private getMessages(): Observable { - return this.dotcmsEventsService - .subscribeTo('LARGE_MESSAGE') + return this.dotEventsSocket + .on('LARGE_MESSAGE') .pipe(filter((data: DotLargeMessageDisplayParams) => !!data)); } } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts index ffc9dd1d25e7..11dc0c46cc80 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts @@ -11,7 +11,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ButtonModule } from 'primeng/button'; -import { DotcmsEventsService, LoginService } from '@dotcms/dotcms-js'; +import { DotEventsSocket } from '@dotcms/data-access'; +import { LoginService } from '@dotcms/dotcms-js'; import { DotMessagePipe } from '@dotcms/ui'; import { DotNotificationItemComponent } from './components/dot-notification-item/dot-notification-item.component'; @@ -34,7 +35,7 @@ import { DotToolbarBtnOverlayComponent } from '../dot-toolbar-overlay/dot-toolba export class DotToolbarNotificationsComponent implements OnInit { readonly #notificationService = inject(NotificationsService); readonly #destroyRef = inject(DestroyRef); - readonly #dotcmsEventsService = inject(DotcmsEventsService); + readonly #dotEventsSocket = inject(DotEventsSocket); readonly #loginService = inject(LoginService); readonly $overlayPanel = viewChild.required('overlayPanel'); @@ -93,8 +94,9 @@ export class DotToolbarNotificationsComponent implements OnInit { } #subscribeToNotifications(): void { - this.#dotcmsEventsService - .subscribeTo('NOTIFICATION') + this.#dotEventsSocket + .on('NOTIFICATION') + .pipe(takeUntilDestroyed(this.#destroyRef)) .subscribe((data: INotification) => { this.$notifications.update((state) => ({ data: [data, ...state.data], diff --git a/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.spec.ts b/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.spec.ts index b8f253de6e4a..5071d1f948c2 100644 --- a/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.spec.ts @@ -1,18 +1,21 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, expect } from '@jest/globals'; +import { Subject } from 'rxjs'; import { TestBed } from '@angular/core/testing'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotMessage, DotMessageSeverity, DotMessageType } from '@dotcms/dotcms-models'; -import { DotcmsEventsServiceMock } from '@dotcms/utils-testing'; import { DotMessageDisplayService } from './dot-message-display.service'; import { DotRouterService } from '../dot-router/dot-router.service'; +import { DotEventsSocket } from '../dot-websocket/dot-events-socket.service'; describe('DotMessageDisplayService', () => { - const mockDotcmsEventsService: DotcmsEventsServiceMock = new DotcmsEventsServiceMock(); + const messageSubject = new Subject(); + const mockDotEventsSocket = { + on: jest.fn().mockReturnValue(messageSubject.asObservable()) + }; let dotMessageDisplayService: DotMessageDisplayService; @@ -28,7 +31,7 @@ describe('DotMessageDisplayService', () => { TestBed.configureTestingModule({ providers: [ DotMessageDisplayService, - { provide: DotcmsEventsService, useValue: mockDotcmsEventsService }, + { provide: DotEventsSocket, useValue: mockDotEventsSocket }, { provide: DotRouterService, useValue: { @@ -44,7 +47,7 @@ describe('DotMessageDisplayService', () => { dotMessageDisplayService = TestBed.inject(DotMessageDisplayService); }); - xit('should emit a message', () => { + it('should emit a message', () => { dotMessageDisplayService.messages().subscribe((message: DotMessage) => { expect(message).toEqual({ ...messageExpected, @@ -53,7 +56,7 @@ describe('DotMessageDisplayService', () => { }); }); - mockDotcmsEventsService.triggerSubscribeTo('MESSAGE', messageExpected); + messageSubject.next(messageExpected); }); it('should push a message', () => { @@ -73,7 +76,7 @@ describe('DotMessageDisplayService', () => { dotMessageDisplayService.unsubscribe(); - mockDotcmsEventsService.triggerSubscribeTo('MESSAGE', messageExpected); + messageSubject.next(messageExpected); expect(wasCalled).toBe(false); }); @@ -90,7 +93,7 @@ describe('DotMessageDisplayService', () => { }); }); - mockDotcmsEventsService.triggerSubscribeTo('MESSAGE', messageExpected); + messageSubject.next(messageExpected); }); it('should not show message when currentPortlet is not in portletIdList ', () => { @@ -102,7 +105,7 @@ describe('DotMessageDisplayService', () => { wasCalled = true; }); - mockDotcmsEventsService.triggerSubscribeTo('MESSAGE', messageExpected); + messageSubject.next(messageExpected); expect(wasCalled).toBe(false); }); diff --git a/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.ts b/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.ts index 2756ec2160f8..5cf4a8cc8833 100644 --- a/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.ts +++ b/core-web/libs/data-access/src/lib/dot-message-display/dot-message-display.service.ts @@ -4,10 +4,10 @@ import { Injectable, inject } from '@angular/core'; import { filter, takeUntil } from 'rxjs/operators'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotMessage, DotMessageSeverity } from '@dotcms/dotcms-models'; import { DotRouterService } from '../dot-router/dot-router.service'; +import { DotEventsSocket } from '../dot-websocket/dot-events-socket.service'; /** * Handle message send by the Backend, this message are sended as Event through the {@link DotcmsEventsService} @@ -18,16 +18,14 @@ import { DotRouterService } from '../dot-router/dot-router.service'; @Injectable() export class DotMessageDisplayService { private dotRouterService = inject(DotRouterService); - private dotcmsEventsService = inject(DotcmsEventsService); + private dotEventsSocket = inject(DotEventsSocket); private messages$: Observable; private destroy$: Subject = new Subject(); private localMessage$: Subject = new Subject(); constructor() { - const webSocketMessage = ( - this.dotcmsEventsService.subscribeTo('MESSAGE') as Observable - ).pipe( + const webSocketMessage = this.dotEventsSocket.on('MESSAGE').pipe( takeUntil(this.destroy$), filter((data: DotMessage) => this.hasPortletIdList(data)) ); From 3f04964429c5f3f3616d41fd9b86e2d94fc4c8e0 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Mon, 16 Mar 2026 11:52:58 -0600 Subject: [PATCH 15/36] fix(websockets): address Copilot review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wire switchSiteEvent$ mock before service creation in DotSiteNavigationEffect spec so the constructor subscription does not receive undefined - Fix GlobalStore SWITCH_SITE test: move mock setup into beforeEach before createService so onInit subscribes to the correct subject - Remove goToSiteBrowser assertion from toolbar test — navigation now lives in DotSiteNavigationEffect, not the toolbar component - Fix misleading onclose comment: 1001 is going-away (tab close), not 1000 Co-Authored-By: Claude Sonnet 4.6 --- .../dot-site-navigation.effect.spec.ts | 25 ++++---- .../iframe-component/iframe.component.spec.ts | 7 ++- .../iframe-component/iframe.component.ts | 6 +- .../dot-toolbar-notifications.component.ts | 2 +- .../dot-toolbar/dot-toolbar.spec.ts | 3 +- .../dot-events-socket.service.ts | 5 +- .../libs/global-store/src/lib/store.spec.ts | 58 +++++++------------ 7 files changed, 47 insertions(+), 59 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts index 6921fb0f7401..ddd06a6a9422 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts @@ -18,26 +18,21 @@ describe('DotSiteNavigationEffect', () => { let dotRouterService: SpyObject; let switchSiteSubject: Subject; - const createService = createServiceFactory({ - service: DotSiteNavigationEffect, - providers: [ - { provide: DotRouterService, useClass: MockDotRouterService }, - mockProvider(GlobalStore, { - switchSiteEvent$: jest.fn() - }) - ] - }); - beforeEach(() => { switchSiteSubject = new Subject(); + const createService = createServiceFactory({ + service: DotSiteNavigationEffect, + providers: [ + { provide: DotRouterService, useClass: MockDotRouterService }, + mockProvider(GlobalStore, { + switchSiteEvent$: jest.fn().mockReturnValue(switchSiteSubject.asObservable()) + }) + ] + }); + spectator = createService(); dotRouterService = spectator.inject(DotRouterService); - - const globalStore = spectator.inject(GlobalStore); - (globalStore.switchSiteEvent$ as jest.Mock).mockReturnValue( - switchSiteSubject.asObservable() - ); }); it('should navigate to site browser when SWITCH_SITE fires on edit page', () => { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts index c71700c952a1..a46945552a3f 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts @@ -45,7 +45,7 @@ describe('IframeComponent', () => { let dotUiColorsService: DotUiColorsService; let dotRouterService: DotRouterService; - const eventSubjects: Record> = {}; + let eventSubjects: Record> = {}; const mockDotEventsSocket = { on: jest.fn((eventType: string) => { if (!eventSubjects[eventType]) { @@ -55,6 +55,11 @@ describe('IframeComponent', () => { }) }; + beforeEach(() => { + eventSubjects = {}; + mockDotEventsSocket.on.mockClear(); + }); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [MockDotLoadingIndicatorComponent], diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts index aff4854d7b59..71a962f0fdff 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts @@ -23,7 +23,7 @@ import { DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { LoggerService } from '@dotcms/dotcms-js'; +import { DotEventTypeWrapper, LoggerService } from '@dotcms/dotcms-js'; import { DotFunctionInfo } from '@dotcms/dotcms-models'; import { DotLoadingIndicatorService } from '@dotcms/utils'; @@ -186,7 +186,9 @@ export class IframeComponent implements OnInit, OnDestroy { ...events.map((eventType) => this.dotEventsSocket .on(eventType) - .pipe(map((data) => ({ data, name: eventType }))) + .pipe( + map((data) => ({ data, name: eventType }) as DotEventTypeWrapper) + ) ) ).pipe(takeUntil(this.destroy$)); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts index 11dc0c46cc80..6212ed79352c 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/dot-toolbar-notifications.component.ts @@ -99,7 +99,7 @@ export class DotToolbarNotificationsComponent implements OnInit { .pipe(takeUntilDestroyed(this.#destroyRef)) .subscribe((data: INotification) => { this.$notifications.update((state) => ({ - data: [data, ...state.data], + data: [data, ...state.data].slice(0, 25), unreadCount: state.unreadCount + 1, hasMore: false })); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts index 6fae2aaa57e6..6eb04600583e 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.spec.ts @@ -163,14 +163,13 @@ describe('DotToolbarComponent', () => { expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled(); }); - it(`should call switchCurrentSite and navigate when on edit page`, () => { + it(`should call switchCurrentSite when on edit page (navigation handled by DotSiteNavigationEffect)`, () => { jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); spectator.detectChanges(); spectator.triggerEventHandler('dot-site', 'onChange', siteMock.identifier); expect(spectator.component.siteChange).toHaveBeenCalledWith(siteMock.identifier); expect(globalStore.switchCurrentSite).toHaveBeenCalledWith(siteMock.identifier); - expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled(); }); }); diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts index 3bea1d284c8c..da5690e148a6 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts @@ -112,8 +112,9 @@ export class DotEventsSocket { this.socket.onclose = (ev: CloseEvent) => { if (!this.destroyed) { - // 1000 = normal closure (server-initiated clean close) — still reconnect - // since dotCMS may restart + // 1001 = "going away" (browser tab/window closing) — don't reconnect. + // All other codes, including 1000 (normal server-side close), still reconnect + // since dotCMS may restart. if (ev.code !== 1001) { this.scheduleReconnect(); } diff --git a/core-web/libs/global-store/src/lib/store.spec.ts b/core-web/libs/global-store/src/lib/store.spec.ts index 36ec3bf7a2eb..121785789caa 100644 --- a/core-web/libs/global-store/src/lib/store.spec.ts +++ b/core-web/libs/global-store/src/lib/store.spec.ts @@ -17,35 +17,32 @@ describe('GlobalStore', () => { let store: InstanceType; let switchSiteSubject: Subject; - const createService = createServiceFactory({ - service: GlobalStore, - providers: [ - mockProvider(DotCurrentUserService), - mockProvider(DotSiteService, { - getCurrentSite: jest.fn().mockReturnValue(of(null)), - switchSite: jest.fn().mockReturnValue(of({})) - }), - mockProvider(DotSystemConfigService), - mockProvider(DotEventsSocket, { - connect: () => of({}), - status$: () => new Subject(), - on: jest.fn().mockReturnValue(new Subject()) - }) - ] - }); - beforeEach(() => { switchSiteSubject = new Subject(); - spectator = createService(); - store = spectator.service; - - // Wire up switchSiteSubject for SWITCH_SITE tests - const eventsSocket = spectator.inject(DotEventsSocket); - (eventsSocket.on as jest.Mock).mockImplementation((event: string) => { - if (event === 'SWITCH_SITE') return switchSiteSubject.asObservable(); - return new Subject(); + const createService = createServiceFactory({ + service: GlobalStore, + providers: [ + mockProvider(DotCurrentUserService), + mockProvider(DotSiteService, { + getCurrentSite: jest.fn().mockReturnValue(of(null)), + switchSite: jest.fn().mockReturnValue(of({})) + }), + mockProvider(DotSystemConfigService), + mockProvider(DotEventsSocket, { + connect: () => of({}), + status$: () => new Subject(), + on: jest.fn().mockImplementation((event: string) => { + if (event === 'SWITCH_SITE') return switchSiteSubject.asObservable(); + + return new Subject(); + }) + }) + ] }); + + spectator = createService(); + store = spectator.service; }); describe('Initial State', () => { @@ -85,17 +82,6 @@ describe('GlobalStore', () => { describe('SWITCH_SITE WebSocket event', () => { it('should update siteDetails when SWITCH_SITE event fires', () => { - // Re-create service after wiring up the mock so onInit picks up the subject - spectator = createService(); - store = spectator.service; - const eventsSocket = spectator.inject(DotEventsSocket); - (eventsSocket.on as jest.Mock).mockImplementation((event: string) => { - if (event === 'SWITCH_SITE') return switchSiteSubject.asObservable(); - - return new Subject(); - }); - - // Trigger the event switchSiteSubject.next(mockSiteEntity); expect(store.siteDetails()).toEqual(mockSiteEntity); From b8c4e9c7bcde6470d9dcd230880a1320c81f909e Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 16:09:18 -0600 Subject: [PATCH 16/36] test(dot-templates): update spec to use GlobalStore mock for site-switch Replace SiteService.switchSite$ with GlobalStore.switchSiteEvent$() in the component test, matching the production code change on this branch. Co-Authored-By: Claude Sonnet 4.6 --- .../dot-template-create-edit.component.spec.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts index cc47477a135b..380d5d4c4425 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { BehaviorSubject, Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -32,7 +32,8 @@ import { PaginatorService } from '@dotcms/data-access'; import { DotcmsEventsService, SiteService } from '@dotcms/dotcms-js'; -import { DotSystemConfig } from '@dotcms/dotcms-models'; +import { DotSite, DotSystemConfig } from '@dotcms/dotcms-models'; +import { GlobalStore } from '@dotcms/store'; import { DotFormDialogComponent, DotMessagePipe, DotApiLinkComponent } from '@dotcms/ui'; import { DotCurrentUserServiceMock, @@ -196,6 +197,12 @@ describe('DotTemplateCreateEditComponent', () => { let store: DotTemplateStore; let templateStoreValue: TemplateStoreValueType; const siteServiceMock = new SiteServiceMock(); + const switchSiteSubject = new Subject(); + + const globalStoreMock = { + switchSiteEvent$: () => switchSiteSubject.asObservable(), + addNewBreadcrumb: jest.fn() + }; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -305,7 +312,8 @@ describe('DotTemplateCreateEditComponent', () => { useValue: new DotcmsEventsServiceMock() }, { provide: DotSystemConfigService, useClass: MockDotSystemConfigService }, - { provide: DotRouterService, useClass: MockDotRouterService } + { provide: DotRouterService, useClass: MockDotRouterService }, + { provide: GlobalStore, useValue: globalStoreMock } ] }) .overrideComponent(DotTemplateCreateEditComponent, { @@ -715,7 +723,7 @@ describe('DotTemplateCreateEditComponent', () => { it('should go to listing if page site changes', () => { fixture.detectChanges(); // Initialize component and subscriptions - siteServiceMock.setFakeCurrentSite(mockSites[1]); // switching the site + switchSiteSubject.next(mockSites[1] as unknown as DotSite); // switching the site expect(store.goToTemplateList).toHaveBeenCalledTimes(1); }); }); From d2323963255ec7910860ed4908fce1c8c44983c6 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 16:13:03 -0600 Subject: [PATCH 17/36] test(dot-template-list): update spec to use GlobalStore mock for site-switch Replace SiteService.switchSite$ with GlobalStore.switchSiteEvent$() in the component test, matching the production code change on this branch. Co-Authored-By: Claude Sonnet 4.6 --- .../dot-template-list.component.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts index 6d088ac3d06d..fb4923fae36a 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts @@ -44,8 +44,10 @@ import { DotContentState, DotMessageSeverity, DotMessageType, + DotSite, DotTemplate } from '@dotcms/dotcms-models'; +import { GlobalStore } from '@dotcms/store'; import { DotActionMenuButtonComponent, DotAddToBundleComponent, @@ -451,6 +453,10 @@ describe('DotTemplateListComponent', () => { const dialogRefClose = new Subject(); const siteServiceMock = new SiteServiceMock(); + const switchSiteSubject = new Subject(); + const globalStoreMock = { + switchSiteEvent$: () => switchSiteSubject.asObservable() + }; beforeEach(async () => { // Create spies for services that will be injected @@ -516,7 +522,8 @@ describe('DotTemplateListComponent', () => { { provide: PushPublishService, useValue: { getEnvironments: jest.fn().mockReturnValue(of([])) } - } + }, + { provide: GlobalStore, useValue: globalStoreMock } ], imports: [ DotTemplateListComponent, @@ -601,10 +608,9 @@ describe('DotTemplateListComponent', () => { it('should reload portlet only when the site change', () => { fixture.detectChanges(); // Initialize component and subscriptions - siteServiceMock.setFakeCurrentSite(mockSites[1]); // switching the site + switchSiteSubject.next(mockSites[1] as unknown as DotSite); // switching the site expect(dotRouterService.gotoPortlet).toHaveBeenCalledWith('templates'); expect(dotRouterService.gotoPortlet).toHaveBeenCalledTimes(1); - expect(dotRouterService.gotoPortlet).toHaveBeenCalledTimes(1); }); it('should set table state (columns, sortField, sortOrder)', () => { From 1264326909395bcaa4e3b457f3b01be5445c4781 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 17:41:25 -0600 Subject: [PATCH 18/36] test(dot-sub-nav): replace real GlobalStore with mockProvider to avoid DotEventsSocket teardown error The real GlobalStore includes withWebSocket() whose onDestroy calls inject(DotEventsSocket) outside an injection context during test cleanup. DotSubNavComponent doesn't use GlobalStore at all, so mocking it is correct. Co-Authored-By: Claude Sonnet 4.6 --- .../components/dot-sub-nav/dot-sub-nav.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/components/dot-sub-nav/dot-sub-nav.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/components/dot-sub-nav/dot-sub-nav.component.spec.ts index 53ed818171e7..f134c0fb3336 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/components/dot-sub-nav/dot-sub-nav.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/components/dot-sub-nav/dot-sub-nav.component.spec.ts @@ -1,4 +1,4 @@ -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -39,7 +39,7 @@ describe('DotSubNavComponent', () => { provide: DotSystemConfigService, useValue: { getSystemConfig: () => ({ of: jest.fn() }) } }, - GlobalStore, + mockProvider(GlobalStore), provideHttpClient(), provideHttpClientTesting() ] From 5b252127d04544c2ba9aced7b8420ec5626ac875 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 17:45:44 -0600 Subject: [PATCH 19/36] fix(with-websocket): avoid inject() in onDestroy default param to prevent NG0203 in tests `inject()` called as a default parameter in `onDestroy` fires during R3Injector.destroy(), after the injection context is gone. Capture the DotEventsSocket reference in withMethods() via a new destroySocket() method and call that from onDestroy() instead. Also adds DotEventsSocket mock to dot-navigation.service.spec.ts so GlobalStore can initialize withWebSocket without opening a real socket. Co-Authored-By: Claude Sonnet 4.6 --- .../services/dot-navigation.service.spec.ts | 10 ++++++++++ .../features/with-websocket/with-websocket.feature.ts | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts index 6a2ed20060c2..8f226a4f2d1f 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts @@ -12,6 +12,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { DotCurrentUserService, DotEventsService, + DotEventsSocket, DotIframeService, DotRouterService, DotSystemConfigService @@ -265,6 +266,15 @@ describe('DotNavigationService', () => { }, { provide: DotCurrentUserService, useClass: DotCurrentUserServiceMock }, GlobalStore, + { + provide: DotEventsSocket, + useValue: { + connect: jest.fn().mockReturnValue(of(null)), + status$: jest.fn().mockReturnValue(of('connected')), + on: jest.fn().mockReturnValue(of()), + destroy: jest.fn() + } + }, provideHttpClient(), provideHttpClientTesting() ], diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts index 8f16b358a9e3..341e835ed8e2 100644 --- a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts @@ -41,6 +41,7 @@ export function withWebSocket() { ) ) ), + destroySocket: () => eventsSocket.destroy(), /** * Observable that emits when the backend sends UPDATE_PORTLET_LAYOUTS. * Use this instead of the deprecated DotcmsEventsService. @@ -75,8 +76,8 @@ export function withWebSocket() { store.startConnection(); store.trackStatus(); }, - onDestroy(_store, eventsSocket = inject(DotEventsSocket)) { - eventsSocket.destroy(); + onDestroy(store) { + store.destroySocket(); } }) ); From 716e8470d7bbebce4f7a188b75e944620158ddb1 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 17:51:28 -0600 Subject: [PATCH 20/36] test(dot-site-navigation): fix spec structure and provider setup Move createServiceFactory to describe level (Spectator requirement) and override GlobalStore per-test via createService() options so each test gets a fresh Subject for switchSiteEvent$. Co-Authored-By: Claude Sonnet 4.6 --- .../dot-site-navigation.effect.spec.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts index ddd06a6a9422..99fa6c43a3a9 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts @@ -18,32 +18,39 @@ describe('DotSiteNavigationEffect', () => { let dotRouterService: SpyObject; let switchSiteSubject: Subject; + const createService = createServiceFactory({ + service: DotSiteNavigationEffect, + providers: [ + { provide: DotRouterService, useClass: MockDotRouterService }, + mockProvider(GlobalStore, { + switchSiteEvent$: jest.fn().mockReturnValue(new Subject()) + }) + ] + }); + beforeEach(() => { switchSiteSubject = new Subject(); - const createService = createServiceFactory({ - service: DotSiteNavigationEffect, + spectator = createService({ providers: [ - { provide: DotRouterService, useClass: MockDotRouterService }, mockProvider(GlobalStore, { switchSiteEvent$: jest.fn().mockReturnValue(switchSiteSubject.asObservable()) }) ] }); - spectator = createService(); dotRouterService = spectator.inject(DotRouterService); }); it('should navigate to site browser when SWITCH_SITE fires on edit page', () => { jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(true); - switchSiteSubject.next(mockSites[0]); + switchSiteSubject.next(mockSites[0] as unknown as DotSite); expect(dotRouterService.goToSiteBrowser).toHaveBeenCalled(); }); it('should NOT navigate when SWITCH_SITE fires on a non-edit page', () => { jest.spyOn(dotRouterService, 'isEditPage').mockReturnValue(false); - switchSiteSubject.next(mockSites[0]); + switchSiteSubject.next(mockSites[0] as unknown as DotSite); expect(dotRouterService.goToSiteBrowser).not.toHaveBeenCalled(); }); }); From 49cfbc6eb597eafb2cbfc4660ed7c4133b9628a0 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 17:54:04 -0600 Subject: [PATCH 21/36] test(container-list): update spec to use GlobalStore mock for site-switch Replace SiteService.switchSite$ with GlobalStore.switchSiteEvent$() in the component test, matching the production code change on this branch. Update site-switch assertion to expect paginatorService.getFirstPage() instead of get() since getContainersByHost calls getFirstPage(). Co-Authored-By: Claude Sonnet 4.6 --- .../container-list.component.spec.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts index 3da6ae11cd92..108168ab9170 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts @@ -1,4 +1,4 @@ -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { CommonModule } from '@angular/common'; import { HttpClient, provideHttpClient } from '@angular/common/http'; @@ -46,7 +46,8 @@ import { SiteService, StringUtils } from '@dotcms/dotcms-js'; -import { CONTAINER_SOURCE, DotActionBulkResult, DotContainer } from '@dotcms/dotcms-models'; +import { CONTAINER_SOURCE, DotActionBulkResult, DotContainer, DotSite } from '@dotcms/dotcms-models'; +import { GlobalStore } from '@dotcms/store'; import { DotActionMenuButtonComponent, DotAddToBundleComponent, @@ -226,10 +227,13 @@ describe('ContainerListComponent', () => { let siteService: SiteServiceMock; let store: DotContainerListStore; let paginatorService: PaginatorService; + let switchSiteSubject: Subject; const messageServiceMock = new MockDotMessageService(messages); beforeEach(async () => { + switchSiteSubject = new Subject(); + await TestBed.configureTestingModule({ declarations: [], imports: [ @@ -295,6 +299,12 @@ describe('ContainerListComponent', () => { { provide: DotMessageDisplayService, useClass: DotMessageDisplayServiceMock + }, + { + provide: GlobalStore, + useValue: { + switchSiteEvent$: () => switchSiteSubject.asObservable() + } } ], schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA] @@ -515,8 +525,9 @@ describe('ContainerListComponent', () => { it("should fetch containers when site is changed and it's not the first time", () => { jest.spyOn(paginatorService, 'setExtraParams'); + jest.spyOn(paginatorService, 'getFirstPage').mockReturnValue(of(containersMock)); - siteService.setFakeCurrentSite(mockSites[1]); + switchSiteSubject.next(mockSites[1] as unknown as DotSite); fixture.detectChanges(); @@ -524,7 +535,7 @@ describe('ContainerListComponent', () => { 'host', mockSites[1].identifier ); - expect(paginatorService.get).toHaveBeenCalled(); + expect(paginatorService.getFirstPage).toHaveBeenCalled(); }); }); From 0612ac0b52f226789b37a1ed2e24ddaf74bff213 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 18:00:29 -0600 Subject: [PATCH 22/36] fix(dot-toolbar): use $currentSite signal alias in template Template was still referencing the old public `globalStore.siteDetails()` property after the store was made private. Update to use the `$currentSite` signal alias exposed on the component. Co-Authored-By: Claude Sonnet 4.6 --- .../app/view/components/dot-toolbar/dot-toolbar.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.html index d50f15b6184c..6ecad74013cc 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.html @@ -6,7 +6,7 @@
Date: Thu, 16 Apr 2026 18:52:13 -0600 Subject: [PATCH 23/36] fix format --- .../container-list/container-list.component.spec.ts | 7 ++++++- core-web/libs/data-access/src/index.ts | 1 - .../src/lib/components/dot-site/dot-site.component.spec.ts | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts index 108168ab9170..5a5ab132d5e2 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts @@ -46,7 +46,12 @@ import { SiteService, StringUtils } from '@dotcms/dotcms-js'; -import { CONTAINER_SOURCE, DotActionBulkResult, DotContainer, DotSite } from '@dotcms/dotcms-models'; +import { + CONTAINER_SOURCE, + DotActionBulkResult, + DotContainer, + DotSite +} from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotActionMenuButtonComponent, diff --git a/core-web/libs/data-access/src/index.ts b/core-web/libs/data-access/src/index.ts index 637bdbd8a59a..24e5d0f81d1f 100644 --- a/core-web/libs/data-access/src/index.ts +++ b/core-web/libs/data-access/src/index.ts @@ -77,4 +77,3 @@ export * from './lib/dot-workflows-actions/dot-workflows-actions.service'; export * from './lib/ema-app-configuration/ema-app-configuration.service'; export * from './lib/paginator/paginator.service'; export * from './lib/push-publish/push-publish.service'; - diff --git a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts index 5cb58b6c9a37..096a55095f05 100644 --- a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts @@ -71,7 +71,6 @@ describe('DotSiteComponent', () => { provideHttpClient(), provideHttpClientTesting() ] - }); beforeEach(() => { From 92fce41778ff05a31c51886f56066164f3ef94cf Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Thu, 16 Apr 2026 20:09:03 -0600 Subject: [PATCH 24/36] fix(global-store, data-access): fix createServiceFactory misuse and MockWebSocket static constants - store.spec.ts: move createServiceFactory to describe level to avoid Spectator "Hooks cannot be defined inside tests" error - dot-events-socket.service.spec.ts: add static CONNECTING/OPEN/CLOSING/CLOSED constants to MockWebSocket so the guard in openSocket() doesn't false-match when global.WebSocket is replaced with the mock (undefined === undefined) Co-Authored-By: Claude Sonnet 4.6 --- .../dot-events-socket.service.spec.ts | 6 ++- .../libs/global-store/src/lib/store.spec.ts | 43 +++++++++---------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts index 4edc1187c437..ad06549b4912 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts @@ -7,12 +7,16 @@ import { DotEventsSocket, WebSocketStatus } from './dot-events-socket.service'; // --------------------------------------------------------------------------- class MockWebSocket { static instances: MockWebSocket[] = []; + static readonly CONNECTING = 0; + static readonly OPEN = 1; + static readonly CLOSING = 2; + static readonly CLOSED = 3; onopen: (() => void) | null = null; onmessage: ((ev: Partial) => void) | null = null; onclose: ((ev: Partial) => void) | null = null; onerror: (() => void) | null = null; - readyState = WebSocket.CONNECTING; + readyState: number = WebSocket.CONNECTING; constructor(public url: string) { MockWebSocket.instances.push(this); diff --git a/core-web/libs/global-store/src/lib/store.spec.ts b/core-web/libs/global-store/src/lib/store.spec.ts index 121785789caa..41c686bb8dff 100644 --- a/core-web/libs/global-store/src/lib/store.spec.ts +++ b/core-web/libs/global-store/src/lib/store.spec.ts @@ -17,30 +17,29 @@ describe('GlobalStore', () => { let store: InstanceType; let switchSiteSubject: Subject; - beforeEach(() => { - switchSiteSubject = new Subject(); - - const createService = createServiceFactory({ - service: GlobalStore, - providers: [ - mockProvider(DotCurrentUserService), - mockProvider(DotSiteService, { - getCurrentSite: jest.fn().mockReturnValue(of(null)), - switchSite: jest.fn().mockReturnValue(of({})) - }), - mockProvider(DotSystemConfigService), - mockProvider(DotEventsSocket, { - connect: () => of({}), - status$: () => new Subject(), - on: jest.fn().mockImplementation((event: string) => { - if (event === 'SWITCH_SITE') return switchSiteSubject.asObservable(); - - return new Subject(); - }) + const createService = createServiceFactory({ + service: GlobalStore, + providers: [ + mockProvider(DotCurrentUserService), + mockProvider(DotSiteService, { + getCurrentSite: jest.fn().mockReturnValue(of(null)), + switchSite: jest.fn().mockReturnValue(of({})) + }), + mockProvider(DotSystemConfigService), + mockProvider(DotEventsSocket, { + connect: () => of({}), + status$: () => new Subject(), + on: jest.fn().mockImplementation((event: string) => { + if (event === 'SWITCH_SITE') return switchSiteSubject.asObservable(); + + return new Subject(); }) - ] - }); + }) + ] + }); + beforeEach(() => { + switchSiteSubject = new Subject(); spectator = createService(); store = spectator.service; }); From 51e515487e7be28a2a3a6707c5624747eca3f32d Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 06:22:09 -0600 Subject: [PATCH 25/36] fix(data-access): make exponential backoff test deterministic by mocking Math.random Co-Authored-By: Claude Sonnet 4.6 --- .../dot-events-socket.service.spec.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts index ad06549b4912..7f09cc4b034d 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts @@ -276,23 +276,30 @@ describe('DotEventsSocket', () => { }); it('should use exponential backoff — second retry waits longer than first', () => { + // Pin jitter to 0 so delays are deterministic: + // retry 1 (retryCount=1): 1000 * 2^1 = 2000ms + // retry 2 (retryCount=2): 1000 * 2^2 = 4000ms + const randomSpy = jest.spyOn(Math, 'random').mockReturnValue(0); + service.connect().subscribe(); latestSocket().triggerOpen(); - // First disconnect + // First disconnect — schedules retry at 2000ms latestSocket().triggerClose(1006); const countAfterFirst = MockWebSocket.instances.length; - jest.advanceTimersByTime(2000); // enough for first retry (1s base + jitter) + jest.advanceTimersByTime(2100); // enough for first retry (2000ms) const countAfterFirstRetry = MockWebSocket.instances.length; - // Second disconnect + // Second disconnect — schedules retry at 4000ms latestSocket().triggerClose(1006); - jest.advanceTimersByTime(2000); // NOT enough for second retry (2s base + jitter) + jest.advanceTimersByTime(3000); // NOT enough for second retry (4000ms) const countAfterShortWait = MockWebSocket.instances.length; - jest.advanceTimersByTime(3000); // now enough + jest.advanceTimersByTime(1100); // now enough (4100ms > 4000ms) const countAfterLongWait = MockWebSocket.instances.length; + randomSpy.mockRestore(); + expect(countAfterFirstRetry).toBeGreaterThan(countAfterFirst); expect(countAfterShortWait).toBe(countAfterFirstRetry); // no new socket yet expect(countAfterLongWait).toBeGreaterThan(countAfterShortWait); From 15671812d4ffac58007d4989741fbecee1352437 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 07:35:08 -0600 Subject: [PATCH 26/36] fix format --- .../components/dot-site/dot-site.component.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts index 096a55095f05..65fa7c30d4b0 100644 --- a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.spec.ts @@ -1061,17 +1061,19 @@ describe('DotSiteComponent', () => { }); describe('WebSocket site events', () => { - // let eventsSocket: SpyObject; + let eventsSocket: SpyObject; let siteEventSubjects: Record>; beforeEach(() => { - // eventsSocket = spectator.inject(DotEventsSocket, true); + eventsSocket = spectator.inject(DotEventsSocket, true); siteEventSubjects = {}; - // eventsSocket.on.mockImplementation((eventType: string): Observable> => { - // siteEventSubjects[eventType] = new Subject>(); - // return siteEventSubjects[eventType].asObservable() as Observable>; - // }); + eventsSocket.on.mockImplementation((eventType: string) => { + siteEventSubjects[eventType] = new Subject<{ identifier: string }>(); + return siteEventSubjects[eventType].asObservable() as unknown as ReturnType< + typeof eventsSocket.on + >; + }); spectator.detectChanges(); }); From 789e5f029021de4e16320e83988023526783949a Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 08:56:16 -0600 Subject: [PATCH 27/36] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20subscription=20leak,=20eager=20init,=20test=20accur?= =?UTF-8?q?acy,=20and=20mock=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - container-list: pipe switchSiteEvent$ through takeUntil(destroy$) to prevent subscription leak on destroy - providers: use provideAppInitializer to force DotSiteNavigationEffect instantiation at startup - dot-site-navigation.effect.spec: remove redundant describe-level mock; clarify beforeEach ordering - dot-events-socket.service: clarify onclose comment (1001 vs 1000 distinction) - dot-events-socket.service.spec: rename test to match what it actually asserts - store.spec: add comment explaining beforeEach ordering; fix switchSite mock type - dot-site.component.spec: restore DotEventsSocket per-event subject wiring (was commented out) Co-Authored-By: Claude Sonnet 4.6 --- .../dot-site-navigation.effect.spec.ts | 9 +++------ .../container-list/container-list.component.ts | 1 + core-web/apps/dotcms-ui/src/app/providers.ts | 13 ++++++++++--- .../dot-websocket/dot-events-socket.service.spec.ts | 5 +---- .../lib/dot-websocket/dot-events-socket.service.ts | 4 ++-- core-web/libs/global-store/src/lib/store.spec.ts | 6 ++++-- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts index 99fa6c43a3a9..e7b474dbd795 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-site-navigation/dot-site-navigation.effect.spec.ts @@ -20,15 +20,12 @@ describe('DotSiteNavigationEffect', () => { const createService = createServiceFactory({ service: DotSiteNavigationEffect, - providers: [ - { provide: DotRouterService, useClass: MockDotRouterService }, - mockProvider(GlobalStore, { - switchSiteEvent$: jest.fn().mockReturnValue(new Subject()) - }) - ] + providers: [{ provide: DotRouterService, useClass: MockDotRouterService }] }); beforeEach(() => { + // switchSiteSubject must be assigned before createService() so the effect + // constructor receives the correct observable when it subscribes on instantiation. switchSiteSubject = new Subject(); spectator = createService({ diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts index 2c78583e0f40..a5ef4d6c06ee 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts @@ -108,6 +108,7 @@ export class ContainerListComponent implements OnDestroy { this.#globalStore .switchSiteEvent$() + .pipe(takeUntil(this.destroy$)) .subscribe(({ identifier }) => this.#store.getContainersByHost(identifier)); } diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index 6c365ee71b68..55c03ac0902f 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -1,4 +1,10 @@ -import { InjectionToken, Provider } from '@angular/core'; +import { + EnvironmentProviders, + inject, + InjectionToken, + Provider, + provideAppInitializer +} from '@angular/core'; import { TitleStrategy } from '@angular/router'; import { ConfirmationService } from 'primeng/api'; @@ -71,7 +77,7 @@ import { DotLoginPageStateService } from './view/components/login/shared/service export const LOCATION_TOKEN = new InjectionToken('Window location object'); -const PROVIDERS: Provider[] = [ +const PROVIDERS: (Provider | EnvironmentProviders)[] = [ { provide: LOCATION_TOKEN, useValue: window.location }, EmaAppConfigurationService, DotAccountService, @@ -138,7 +144,8 @@ const PROVIDERS: Provider[] = [ }, GlobalStore, DotSystemConfigService, - DotSiteNavigationEffect + DotSiteNavigationEffect, + provideAppInitializer(() => void inject(DotSiteNavigationEffect)) ]; export const ENV_PROVIDERS = [...PROVIDERS]; diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts index 7f09cc4b034d..583fa0ff7d61 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.spec.ts @@ -123,7 +123,7 @@ describe('DotEventsSocket', () => { expect(statuses[statuses.length - 1]).toBe('connected'); }); - it('should reset retryCount on successful open', () => { + it('should report connected after a successful reconnect', () => { service.connect().subscribe(); latestSocket().triggerOpen(); latestSocket().triggerClose(); @@ -131,9 +131,6 @@ describe('DotEventsSocket', () => { latestSocket().triggerOpen(); - // After reconnect succeeds, status goes back to connected - const statuses: WebSocketStatus[] = []; - service.status$().subscribe((s) => statuses.push(s)); expect(service.isConnected()).toBe(true); }); }); diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts index da5690e148a6..ad47b1b14069 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts @@ -113,8 +113,8 @@ export class DotEventsSocket { this.socket.onclose = (ev: CloseEvent) => { if (!this.destroyed) { // 1001 = "going away" (browser tab/window closing) — don't reconnect. - // All other codes, including 1000 (normal server-side close), still reconnect - // since dotCMS may restart. + // All other codes (including 1000, normal closure) still reconnect + // since dotCMS may restart after a clean shutdown. if (ev.code !== 1001) { this.scheduleReconnect(); } diff --git a/core-web/libs/global-store/src/lib/store.spec.ts b/core-web/libs/global-store/src/lib/store.spec.ts index 41c686bb8dff..f7f6754421b1 100644 --- a/core-web/libs/global-store/src/lib/store.spec.ts +++ b/core-web/libs/global-store/src/lib/store.spec.ts @@ -23,7 +23,7 @@ describe('GlobalStore', () => { mockProvider(DotCurrentUserService), mockProvider(DotSiteService, { getCurrentSite: jest.fn().mockReturnValue(of(null)), - switchSite: jest.fn().mockReturnValue(of({})) + switchSite: jest.fn().mockReturnValue(of({} as DotSite)) }), mockProvider(DotSystemConfigService), mockProvider(DotEventsSocket, { @@ -39,6 +39,8 @@ describe('GlobalStore', () => { }); beforeEach(() => { + // switchSiteSubject is assigned before createService() so the on() closure + // captures the correct subject by the time onInit subscribes to SWITCH_SITE. switchSiteSubject = new Subject(); spectator = createService(); store = spectator.service; @@ -68,7 +70,7 @@ describe('GlobalStore', () => { it('should call switchSite then getCurrentSite and update siteDetails', () => { const siteService = spectator.inject(DotSiteService); const newSite: DotSite = { ...mockSiteEntity, identifier: 'new-site' }; - siteService.switchSite.mockReturnValue(of({})); + siteService.switchSite.mockReturnValue(of({} as DotSite)); siteService.getCurrentSite.mockReturnValue(of(newSite)); store.switchCurrentSite('new-site'); From defaf1577050c9247e3d566cf7db5ef29e43145b Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 10:57:12 -0600 Subject: [PATCH 28/36] feat(websocket): remove legacy DotcmsEventsService stack to eliminate dual WebSocket connections Migrates the last 4 consumers to use DotEventsSocket from @dotcms/data-access or GlobalStore event streams, then deletes the entire legacy WebSocket layer. - LoginService: SESSION_DESTROYED/SESSION_LOGOUT now wired to DotEventsSocket directly - RoutingService: UPDATE_PORTLET_LAYOUTS now wired to DotEventsSocket (avoids circular dep) - SiteService: event subscriptions removed (data API unchanged); deprecated class kept - DotPluginsListStore: OSGI events migrated from DotcmsEventsService to DotEventsSocket - Removed DotcmsEventsService, DotEventsSocket (legacy), DotEventsSocketURL from all provider arrays - Deleted: dotcms-events.service.ts, dot-event-socket.ts, long-polling-protocol.ts, websockets-protocol.ts, dot-event-socket-url model, and all related spec files - Cleaned barrel exports in dotcms-js/src/public_api.ts - Updated ~60 spec files to remove dead provider mocks for the deleted services Co-Authored-By: Claude Sonnet 4.6 --- core-web/apps/dotcdn/src/app/app.module.ts | 13 - .../dot-custom-event-handler.service.spec.ts | 8 +- .../container-list.component.spec.ts | 7 - .../dot-add-variable.component.spec.ts | 15 +- .../dot-container-create.component.spec.ts | 15 +- ...dot-container-properties.component.spec.ts | 14 +- .../dot-pages-store/dot-pages.store.spec.ts | 3 - .../dot-pages-table.component.spec.ts | 8 +- .../dot-contentlets.component.spec.ts | 8 +- .../dot-portlet-detail.component.spec.ts | 9 +- .../dot-workflow-task.component.spec.ts | 8 +- ...dot-template-create-edit.component.spec.ts | 7 +- .../dot-template-props.component.spec.ts | 7 +- .../dot-template-list.component.spec.ts | 7 - ...nt-type-fields-drop-zone.component.spec.ts | 4 +- .../form/content-types-form.component.spec.ts | 5 - .../content-types-layout.component.spec.ts | 9 +- ...content-type-copy-dialog.component.spec.ts | 5 - core-web/apps/dotcms-ui/src/app/providers.ts | 4 - .../dotcms-ui/src/app/shared/shared.module.ts | 13 - .../dotcms-ui/src/app/test/dot-test-bed.ts | 13 - .../dot-wizard/dot-wizard.component.spec.ts | 9 +- .../iframe-porlet-legacy.component.spec.ts | 6 +- .../dot-add-persona-dialog.component.spec.ts | 8 +- .../dot-add-contentlet.component.spec.ts | 8 +- .../dot-contentlet-wrapper.component.spec.ts | 15 +- .../dot-create-contentlet.component.spec.ts | 9 +- .../dot-reorder-menu.component.spec.ts | 10 +- .../dot-iframe-dialog.component.spec.ts | 19 +- .../services/dot-navigation.service.spec.ts | 28 +- .../dot-persona-selector.component.spec.ts | 8 +- .../dot-notification-item.component.spec.ts | 21 +- .../store/dot-toolbar-user.store.spec.ts | 14 +- ...dot-workflow-task-detail.component.spec.ts | 14 +- .../main-legacy/main-legacy.component.spec.ts | 8 +- ...dot-workflow-event-handler.service.spec.ts | 10 +- .../dot-rules/src/lib/rule-engine.module.ts | 2 - .../lib/core/dotcms-events.service.spec.ts | 167 ------------ .../src/lib/core/dotcms-events.service.ts | 107 -------- .../dotcms-js/src/lib/core/login.service.ts | 33 +-- .../dotcms-js/src/lib/core/routing.service.ts | 12 +- .../dotcms-js/src/lib/core/site.service.ts | 14 - .../lib/core/util/dot-event-socket.spec.ts | 142 ----------- .../src/lib/core/util/dot-event-socket.ts | 240 ------------------ .../core/util/long-polling-protocol.spec.ts | 102 -------- .../lib/core/util/long-polling-protocol.ts | 93 ------- .../core/util/models/dot-event-socket-url.ts | 39 --- .../lib/core/util/websockets-protocol.spec.ts | 68 ----- .../src/lib/core/util/websockets-protocol.ts | 70 ----- core-web/libs/dotcms-js/src/public_api.ts | 5 - .../dot-experiments-list.component.spec.ts | 2 - .../dot-plugins-list.component.spec.ts | 5 +- .../store/dot-plugins-list.store.spec.ts | 6 +- .../store/dot-plugins-list.store.ts | 16 +- .../dot-ema-dialog.component.spec.ts | 12 +- .../dot-ema-shell.component.spec.ts | 13 +- .../edit-ema-editor.component.spec.ts | 7 +- .../dot-ema-workflow-actions.service.spec.ts | 10 +- ...template-builder-actions.component.spec.ts | 7 +- .../template-builder.component.spec.ts | 7 +- core-web/libs/utils-testing/src/index.ts | 1 - .../src/lib/dotcms-events-service.mock.ts | 40 --- 62 files changed, 80 insertions(+), 1509 deletions(-) delete mode 100644 core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.spec.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.spec.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.spec.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/util/models/dot-event-socket-url.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.spec.ts delete mode 100644 core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.ts delete mode 100644 core-web/libs/utils-testing/src/lib/dotcms-events-service.mock.ts diff --git a/core-web/apps/dotcdn/src/app/app.module.ts b/core-web/apps/dotcdn/src/app/app.module.ts index 567eb91cf2fe..79a83c4e192f 100644 --- a/core-web/apps/dotcdn/src/app/app.module.ts +++ b/core-web/apps/dotcdn/src/app/app.module.ts @@ -15,9 +15,6 @@ import { TextareaModule } from 'primeng/textarea'; import { DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, LoggerService, LoginService, SiteService, @@ -28,13 +25,6 @@ import { DotIconComponent, DotSpinnerComponent } from '@dotcms/ui'; import { AppComponent } from './app.component'; import { DotCDNStore } from './dotcdn.component.store'; -const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); -}; - @NgModule({ declarations: [AppComponent], imports: [ @@ -59,9 +49,6 @@ const dotEventSocketURLFactory = () => { StringUtils, SiteService, LoginService, - DotEventsSocket, - DotcmsEventsService, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, DotCDNStore ], diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts index 57106dacc661..c8a2180da10e 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { of } from 'rxjs'; @@ -33,9 +34,6 @@ import { import { ApiRoot, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, LoginService, @@ -52,7 +50,6 @@ import { import { DotCustomEventHandlerService } from './dot-custom-event-handler.service'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../test/dot-test-bed'; import { DotContentletEditorService } from '../../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; import { DotDownloadBundleDialogService } from '../dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from '../dot-menu.service'; @@ -94,10 +91,7 @@ describe('DotCustomEventHandlerService', () => { { provide: DotFormatDateService, useClass: DotFormatDateServiceMock }, UserModel, StringUtils, - DotcmsEventsService, LoggerService, - DotEventsSocket, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, LoggerService, DotCurrentUserService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts index 5a5ab132d5e2..8ac56cd8be57 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts @@ -36,9 +36,6 @@ import { } from '@dotcms/data-access'; import { DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, LoginService, @@ -73,7 +70,6 @@ import { ContainerListComponent } from './container-list.component'; import { DotContainerListStore } from './store/dot-container-list.store'; import { DotContainersService } from '../../../api/services/dot-containers/dot-containers.service'; -import { dotEventSocketURLFactory } from '../../../test/dot-test-bed'; import { DotEmptyStateComponent } from '../../../view/components/_common/dot-empty-state/dot-empty-state.component'; import { DotContentTypeSelectorComponent } from '../../../view/components/dot-content-type-selector/dot-content-type-selector.component'; import { ActionHeaderComponent } from '../../../view/components/dot-listing-data-table/action-header/action-header.component'; @@ -267,10 +263,8 @@ describe('ContainerListComponent', () => { DialogService, DotAlertConfirmService, DotcmsConfigService, - DotcmsEventsService, DotContainerListStore, DotContainersService, - DotEventsSocket, DotHttpErrorManagerService, DotSiteBrowserService, HttpClient, @@ -299,7 +293,6 @@ describe('ContainerListComponent', () => { } }, { provide: DotMessageService, useValue: messageServiceMock }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: DotFormatDateService, useClass: DotFormatDateServiceMock }, { provide: DotMessageDisplayService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.spec.ts index f114c17565e2..501b562dd91c 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.spec.ts @@ -31,15 +31,7 @@ import { DotSiteBrowserService, DotGlobalMessageService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - LoginService, - StringUtils -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { DotCMSContentType } from '@dotcms/dotcms-models'; import { DotMessagePipe } from '@dotcms/ui'; import { @@ -52,8 +44,6 @@ import { DotAddVariableComponent } from './dot-add-variable.component'; import { FilteredFieldTypes } from './dot-add-variable.models'; import { DOT_CONTENT_MAP, DotFieldsService } from './services/dot-fields.service'; -import { dotEventSocketURLFactory } from '../../../../../test/dot-test-bed'; - @Component({ selector: 'dot-form-dialog', template: '', @@ -227,13 +217,10 @@ describe('DotAddVariableComponent', () => { } } }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, StringUtils, DotHttpErrorManagerService, DotAlertConfirmService, ConfirmationService, - DotcmsEventsService, - DotEventsSocket, DotcmsConfigService, { provide: DotMessageDisplayService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-create.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-create.component.spec.ts index cf7735bc31ab..a5b6c78b8af3 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-create.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-create.component.spec.ts @@ -17,20 +17,12 @@ import { DotMessageDisplayService, DotRouterService } from '@dotcms/data-access'; -import { - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - StringUtils -} from '@dotcms/dotcms-js'; +import { LoggerService, StringUtils } from '@dotcms/dotcms-js'; import { CONTAINER_SOURCE } from '@dotcms/dotcms-models'; import { DotMessagePipe } from '@dotcms/ui'; import { DotContainerCreateComponent } from './dot-container-create.component'; -import { dotEventSocketURLFactory } from '../../../test/dot-test-bed'; - @Pipe({ name: 'dm' }) @@ -91,12 +83,9 @@ describe('ContainerCreateComponent', () => { DotGlobalMessageService, DotHttpErrorManagerService, DotMessageDisplayService, - DotcmsEventsService, - DotEventsSocket, LoggerService, StringUtils, - DotContentTypeService, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory } + DotContentTypeService ] }) .overrideComponent(DotContainerCreateComponent, { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/dot-container-properties.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/dot-container-properties.component.spec.ts index 18b5bfa2b180..b133e04e3dd4 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/dot-container-properties.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/dot-container-properties.component.spec.ts @@ -42,15 +42,7 @@ import { DotRouterService, DotSiteBrowserService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - LoginService, - StringUtils -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { DotCMSContentType } from '@dotcms/dotcms-models'; import { DotActionMenuButtonComponent, @@ -72,7 +64,6 @@ import { import { DotContainerPropertiesComponent } from './dot-container-properties.component'; import { DotContainersService } from '../../../../api/services/dot-containers/dot-containers.service'; -import { dotEventSocketURLFactory } from '../../../../test/dot-test-bed'; import { DotActionButtonComponent } from '../../../../view/components/_common/dot-action-button/dot-action-button.component'; @Component({ @@ -253,7 +244,6 @@ describe('DotContainerPropertiesComponent', () => { provideHttpClient(), provideHttpClientTesting(), { provide: DotMessageService, useValue: messageServiceMock }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: ActivatedRoute, useValue: { data: of(containerMockData) } }, { provide: DotRouterService, useValue: mockRouterService }, StringUtils, @@ -262,8 +252,6 @@ describe('DotContainerPropertiesComponent', () => { DotAlertConfirmService, ConfirmationService, LoginService, - DotcmsEventsService, - DotEventsSocket, DotcmsConfigService, { provide: DotMessageDisplayService, useClass: DotMessageDisplayServiceMock }, DialogService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts index 6a6b0379719f..3fe120d99546 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts @@ -32,7 +32,6 @@ import { } from '@dotcms/data-access'; import { DotcmsConfigService, - DotcmsEventsService, DotPushPublishDialogService, LoggerService, LoginService, @@ -52,7 +51,6 @@ import { DotcmsConfigServiceMock, dotcmsContentletMock, dotcmsContentTypeBasicMock, - DotcmsEventsServiceMock, DotCurrentUserServiceMock, DotLanguagesServiceMock, DotLicenseServiceMock, @@ -163,7 +161,6 @@ describe('DotPageStore', () => { DotLocalstorageService, DotPushPublishDialogService, { provide: DialogService, useClass: DialogServiceMock }, - { provide: DotcmsEventsService, useClass: DotcmsEventsServiceMock }, { provide: DotCurrentUserService, useClass: DotCurrentUserServiceMock }, { provide: DotMessageDisplayService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-table/dot-pages-table.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-table/dot-pages-table.component.spec.ts index 8c17be842f3e..8cf46c00c23b 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-table/dot-pages-table.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-table/dot-pages-table.component.spec.ts @@ -14,7 +14,6 @@ import { TableModule } from 'primeng/table'; import { TooltipModule } from 'primeng/tooltip'; import { DotFormatDateService, DotMessageService } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotCMSContentlet, DotSystemLanguage } from '@dotcms/dotcms-models'; import { DotAutofocusDirective, @@ -22,7 +21,6 @@ import { DotMessagePipe, DotRelativeDatePipe } from '@dotcms/ui'; -import { DotcmsEventsServiceMock } from '@dotcms/utils-testing'; import { DotPagesTableComponent } from './dot-pages-table.component'; @@ -162,11 +160,7 @@ describe('DotPagesTableComponent', () => { MockProvider(DotFormatDateService), MockProvider(DotPageActionsService, { getItems: jest.fn().mockReturnValue(of([])) - }), - { - provide: DotcmsEventsService, - useClass: DotcmsEventsServiceMock - } + }) ] }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts index 4348fa49dc6a..3dcac85ea7b1 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; @@ -34,9 +35,6 @@ import { import { ApiRoot, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, LoggerService, LoginService, StringUtils, @@ -52,7 +50,6 @@ import { DotContentletsComponent } from './dot-contentlets.component'; import { DotCustomEventHandlerService } from '../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotDownloadBundleDialogService } from '../../../api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../view/components/_common/iframe/service/iframe-overlay.service'; import { DotEditContentletComponent } from '../../../view/components/dot-contentlet-editor/components/dot-edit-contentlet/dot-edit-contentlet.component'; import { DotContentletEditorService } from '../../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; @@ -101,10 +98,7 @@ describe('DotContentletsComponent', () => { DotFormatDateService, UserModel, StringUtils, - DotcmsEventsService, LoggerService, - DotEventsSocket, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: DotcmsConfigService, useClass: DotcmsConfigServiceMock }, DotCurrentUserService, DotMessageDisplayService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts index 3650bcaa019d..076c147a9c32 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts @@ -33,9 +33,6 @@ import { import { ApiRoot, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, LoginService, @@ -46,10 +43,11 @@ import { LoginServiceMock, MockDotRouterService } from '@dotcms/utils-testing'; import { DotPortletDetailComponent } from './dot-portlet-detail.component'; +import { MockDotUiColorsService } from '../../test/dot-test-bed'; + import { DotCustomEventHandlerService } from '../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotDownloadBundleDialogService } from '../../api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from '../../api/services/dot-menu.service'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../test/dot-test-bed'; import { DotDownloadBundleDialogComponent } from '../../view/components/_common/dot-download-bundle-dialog/dot-download-bundle-dialog.component'; import { IframeOverlayService } from '../../view/components/_common/iframe/service/iframe-overlay.service'; import { DotContentletEditorService } from '../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; @@ -77,10 +75,7 @@ describe('DotPortletDetailComponent', () => { DotFormatDateService, UserModel, StringUtils, - DotcmsEventsService, LoggerService, - DotEventsSocket, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, LoggerService, DotCurrentUserService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts index b01cf1740bdc..fdd6c50ee65d 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { mockProvider } from '@ngneat/spectator/jest'; @@ -38,9 +39,6 @@ import { import { ApiRoot, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, LoggerService, LoginService, StringUtils, @@ -58,7 +56,6 @@ import { DotWorkflowTaskComponent } from './dot-workflow-task.component'; import { DotCustomEventHandlerService } from '../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotDownloadBundleDialogService } from '../../../api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from '../../../api/services/dot-menu.service'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../view/components/_common/iframe/service/iframe-overlay.service'; import { DotContentletEditorService } from '../../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; import { DotWorkflowTaskDetailComponent } from '../../../view/components/dot-workflow-task-detail/dot-workflow-task-detail.component'; @@ -133,9 +130,6 @@ describe('DotWorkflowTaskComponent', () => { provideHttpClientTesting(), DotCurrentUserService, DotMessageDisplayService, - DotcmsEventsService, - DotEventsSocket, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, DotWizardService, DotHttpErrorManagerService, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts index 380d5d4c4425..3b76a00a7c3b 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts @@ -31,13 +31,12 @@ import { DotWorkflowActionsFireService, PaginatorService } from '@dotcms/data-access'; -import { DotcmsEventsService, SiteService } from '@dotcms/dotcms-js'; +import { SiteService } from '@dotcms/dotcms-js'; import { DotSite, DotSystemConfig } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotFormDialogComponent, DotMessagePipe, DotApiLinkComponent } from '@dotcms/ui'; import { DotCurrentUserServiceMock, - DotcmsEventsServiceMock, MockDotMessageService, MockDotRouterService, mockDotThemes, @@ -307,10 +306,6 @@ describe('DotTemplateCreateEditComponent', () => { get: jest.fn().mockReturnValue(of(mockDotThemes[1])) } }, - { - provide: DotcmsEventsService, - useValue: new DotcmsEventsServiceMock() - }, { provide: DotSystemConfigService, useClass: MockDotSystemConfigService }, { provide: DotRouterService, useClass: MockDotRouterService }, { provide: GlobalStore, useValue: globalStoreMock } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-props/dot-template-props.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-props/dot-template-props.component.spec.ts index b33e85c974c9..0b1ec00e778e 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-props/dot-template-props.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-props/dot-template-props.component.spec.ts @@ -14,14 +14,13 @@ import { ButtonModule } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { DotMessageService } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotFieldRequiredDirective, DotFieldValidationMessageComponent, DotMessagePipe, DotThemeComponent } from '@dotcms/ui'; -import { DotcmsEventsServiceMock, MockDotMessageService } from '@dotcms/utils-testing'; +import { MockDotMessageService } from '@dotcms/utils-testing'; import { DotTemplatePropsComponent } from './dot-template-props.component'; import { DotTemplateThumbnailFieldComponent } from './dot-template-thumbnail-field/dot-template-thumbnail-field.component'; @@ -116,10 +115,6 @@ describe('DotTemplatePropsComponent', () => { provide: DotMessageService, useValue: messageServiceMock }, - { - provide: DotcmsEventsService, - useValue: new DotcmsEventsServiceMock() - }, { provide: DynamicDialogRef, useValue: { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts index fb4923fae36a..86d4498ec62a 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts @@ -30,9 +30,6 @@ import { } from '@dotcms/data-access'; import { DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, LoginService, @@ -101,7 +98,6 @@ afterAll(() => { }); import { DotTemplatesService } from '../../../api/services/dot-templates/dot-templates.service'; -import { dotEventSocketURLFactory } from '../../../test/dot-test-bed'; import { DotActionButtonComponent } from '../../../view/components/_common/dot-action-button/dot-action-button.component'; import { DotBulkInformationComponent } from '../../../view/components/_common/dot-bulk-information/dot-bulk-information.component'; @@ -494,7 +490,6 @@ describe('DotTemplateListComponent', () => { provide: ActivatedRoute, useClass: ActivatedRouteMock }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: DotRouterService, useValue: dotRouterServiceSpy @@ -507,8 +502,6 @@ describe('DotTemplateListComponent', () => { DotHttpErrorManagerService, DotAlertConfirmService, ConfirmationService, - DotcmsEventsService, - DotEventsSocket, DotcmsConfigService, DotMessageDisplayService, { provide: DialogService, useValue: dialogServiceSpy }, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts index f17d1c8b2f9f..5701d69dd65f 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts @@ -26,7 +26,7 @@ import { DotMessageDisplayService, DotMessageService } from '@dotcms/data-access'; -import { DotEventsSocket, LoginService } from '@dotcms/dotcms-js'; +import { LoginService } from '@dotcms/dotcms-js'; import { DotCMSClazzes, DotCMSContentType, @@ -281,7 +281,6 @@ describe('ContentTypeFieldsDropZoneComponent', () => { provide: DotLoadingIndicatorService, useValue: dotLoadingIndicatorServiceMock }, - DotEventsSocket, LoginService, DotFormatDateService, FieldService, @@ -585,7 +584,6 @@ describe('Load fields and drag and drop', () => { }, DotFormatDateService, LoginService, - DotEventsSocket, { provide: DotMessageService, useValue: messageServiceMock }, { provide: FieldDragDropService, useValue: testFieldDragDropService }, { provide: Router, useValue: mockRouter }, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts index 8771e3d703ce..18ec877400f1 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts @@ -17,7 +17,6 @@ import { DotWorkflowsActionsService, DotWorkflowService } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotCMSClazzes, DotCMSContentTypeLayoutRow, @@ -162,10 +161,6 @@ describe('ContentTypesFormComponent', () => { { provide: DotWorkflowService, useClass: DotWorkflowServiceMock }, { provide: DotLicenseService, useClass: MockDotLicenseService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, - { - provide: DotcmsEventsService, - useValue: { subscribeToEvents: jest.fn().mockReturnValue(EMPTY) } - }, mockProvider(DotHttpErrorManagerService), mockProvider(DotWorkflowsActionsService), { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.spec.ts index 20309467bbec..90d25e174e85 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.spec.ts @@ -39,7 +39,7 @@ import { DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoggerService, LoginService } from '@dotcms/dotcms-js'; +import { LoggerService, LoginService } from '@dotcms/dotcms-js'; import { DotCMSContentType } from '@dotcms/dotcms-models'; import { DotApiLinkComponent, @@ -199,13 +199,6 @@ describe('ContentTypesLayoutComponent', () => { useValue: { currentPortlet: { id: 'test-portlet-id' } } }, { provide: DotUiColorsService, useValue: { setColors: jest.fn() } }, - { - provide: DotcmsEventsService, - useValue: { - subscribeTo: jest.fn().mockReturnValue(of({})), - subscribeToEvents: jest.fn().mockReturnValue(of({})) - } - }, { provide: DotLoadingIndicatorService, useValue: { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts index 7c7a5f2de20d..061daae08562 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts @@ -9,7 +9,6 @@ import { By } from '@angular/platform-browser'; import { provideAnimations } from '@angular/platform-browser/animations'; import { DotMessageService, DotSiteService } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotFieldValidationMessageComponent, DotMessagePipe, DotSiteComponent } from '@dotcms/ui'; import { MockDotMessageService } from '@dotcms/utils-testing'; @@ -68,10 +67,6 @@ describe('DotContentTypeCopyDialogComponent', () => { getSites: jest.fn().mockReturnValue(of({})) } }, - { - provide: DotcmsEventsService, - useValue: { subscribeToEvents: jest.fn().mockReturnValue(EMPTY) } - }, provideHttpClient(), provideHttpClientTesting(), provideAnimations() diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index 55c03ac0902f..672e2f3686cf 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -19,7 +19,6 @@ import { DotCrudService, DotCurrentUserService, DotEventsService, - DotEventsSocket, DotFormatDateService, DotGenerateSecurePasswordService, DotGlobalMessageService, @@ -44,7 +43,6 @@ import { ApiRoot, BrowserUtil, DotcmsConfigService, - DotcmsEventsService, DotPushPublishDialogService, LoggerService, LoginService, @@ -122,10 +120,8 @@ const PROVIDERS: (Provider | EnvironmentProviders)[] = [ DotEventsService, DotNavigationService, DotcmsConfigService, - DotcmsEventsService, LoggerService, LoginService, - DotEventsSocket, StringUtils, UserModel, // Data-access services diff --git a/core-web/apps/dotcms-ui/src/app/shared/shared.module.ts b/core-web/apps/dotcms-ui/src/app/shared/shared.module.ts index be1809586053..4e37bf03a9c1 100644 --- a/core-web/apps/dotcms-ui/src/app/shared/shared.module.ts +++ b/core-web/apps/dotcms-ui/src/app/shared/shared.module.ts @@ -7,9 +7,6 @@ import { ApiRoot, BrowserUtil, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, LoggerService, LoginService, StringUtils, @@ -19,13 +16,6 @@ import { import { DotNavigationComponent } from '../view/components/dot-navigation/dot-navigation.component'; import { DotNavigationService } from '../view/components/dot-navigation/services/dot-navigation.service'; -const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); -}; - @NgModule({ declarations: [], imports: [CommonModule, DotNavigationComponent], @@ -45,11 +35,8 @@ export class SharedModule { DotEventsService, DotNavigationService, DotcmsConfigService, - DotcmsEventsService, LoggerService, LoginService, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, - DotEventsSocket, StringUtils, UserModel ] diff --git a/core-web/apps/dotcms-ui/src/app/test/dot-test-bed.ts b/core-web/apps/dotcms-ui/src/app/test/dot-test-bed.ts index fb9f543416fe..45f84eb8dc22 100644 --- a/core-web/apps/dotcms-ui/src/app/test/dot-test-bed.ts +++ b/core-web/apps/dotcms-ui/src/app/test/dot-test-bed.ts @@ -26,9 +26,6 @@ import { ApiRoot, BrowserUtil, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, StringUtils, @@ -83,13 +80,6 @@ export class MockGlobalStore { }; } -export const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); -}; - /** * DOTTestBed its deprecated * @deprecated This class is deprecated @@ -125,10 +115,7 @@ export class DOTTestBed { DotHttpErrorManagerService, DotIframeService, DotMessageService, - DotEventsSocket, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, - DotcmsEventsService, DotFormatDateService, LoggerService, StringUtils, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-wizard/dot-wizard.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-wizard/dot-wizard.component.spec.ts index 79262adff27c..b8f22d24f9d3 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-wizard/dot-wizard.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-wizard/dot-wizard.component.spec.ts @@ -26,13 +26,7 @@ import { DotWizardService, PushPublishService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - LoggerService, - LoginService, - StringUtils -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { DotPushPublishDialogData, DotWizardInput, DotWizardStep } from '@dotcms/dotcms-models'; import { LoginServiceMock, MockDotMessageService } from '@dotcms/utils-testing'; @@ -148,7 +142,6 @@ describe('DotWizardComponent', () => { DotPushPublishFiltersService, DotParseHtmlService, DotcmsConfigService, - DotcmsEventsService, DotWizardService ] }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts index eb4a971ef62d..a6b94522f06f 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { EMPTY, of } from 'rxjs'; @@ -34,8 +35,6 @@ import { import { ApiRoot, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, LoginService, @@ -49,7 +48,6 @@ import { IframePortletLegacyComponent } from './iframe-porlet-legacy.component'; import { DotCustomEventHandlerService } from '../../../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotMenuService } from '../../../../../api/services/dot-menu.service'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { DotContentletEditorService } from '../../../dot-contentlet-editor/services/dot-contentlet-editor.service'; import { DotDownloadBundleDialogComponent } from '../../dot-download-bundle-dialog/dot-download-bundle-dialog.component'; import { IFrameModule } from '../index'; @@ -116,12 +114,10 @@ xdescribe('IframePortletLegacyComponent', () => { StringUtils, DotCurrentUserService, DotMessageDisplayService, - DotcmsEventsService, { provide: DotEventsSocketDataAccess, useValue: { on: jest.fn().mockReturnValue(EMPTY) } }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, DotFormatDateService, DotWizardService, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts index c3c375ba9b90..0fef810ce93e 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts @@ -9,7 +9,7 @@ import { DotMessageService, DotWorkflowActionsFireService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoginService, SiteService } from '@dotcms/dotcms-js'; +import { LoginService, SiteService } from '@dotcms/dotcms-js'; import { GlobalStore } from '@dotcms/store'; import { DotMessageDisplayServiceMock, @@ -47,11 +47,7 @@ describe('DotAddPersonaDialogComponent', () => { { provide: DotMessageService, useValue: messageServiceMock }, { provide: LoginService, useClass: LoginServiceMock }, { provide: SiteService, useValue: new SiteServiceMock() }, - mockProvider(GlobalStore, { currentSiteId: jest.fn().mockReturnValue('demo') }), - { - provide: DotcmsEventsService, - useValue: { subscribeToEvents: jest.fn().mockReturnValue(EMPTY) } - } + mockProvider(GlobalStore, { currentSiteId: jest.fn().mockReturnValue('demo') }) ] }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts index 32859ae52192..e097b0fc2b6b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; @@ -23,9 +24,6 @@ import { import { ApiRoot, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, LoggerService, LoginService, StringUtils, @@ -42,7 +40,6 @@ import { DotAddContentletComponent } from './dot-add-contentlet.component'; import { DotCustomEventHandlerService } from '../../../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotMenuService } from '../../../../../api/services/dot-menu.service'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../_common/iframe/service/iframe-overlay.service'; import { DotIframeDialogComponent } from '../../../dot-iframe-dialog/dot-iframe-dialog.component'; import { DotContentletEditorService } from '../../services/dot-contentlet-editor.service'; @@ -96,9 +93,6 @@ describe('DotAddContentletComponent', () => { toggle: jest.fn() } }, - DotcmsEventsService, - DotEventsSocket, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, LoggerService, StringUtils, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts index f8a10f4e90f8..a9d443fff2d6 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { of } from 'rxjs'; @@ -21,15 +22,7 @@ import { DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - LoginService, - StringUtils -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { LoginServiceMock, MockDotMessageService, @@ -40,7 +33,6 @@ import { DotContentletWrapperComponent } from './dot-contentlet-wrapper.componen import { DotCustomEventHandlerService } from '../../../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotMenuService } from '../../../../../api/services/dot-menu.service'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../_common/iframe/service/iframe-overlay.service'; import { DotIframeDialogComponent } from '../../../dot-iframe-dialog/dot-iframe-dialog.component'; import { DotContentletEditorService } from '../../services/dot-contentlet-editor.service'; @@ -82,13 +74,10 @@ describe('DotContentletWrapperComponent', () => { DotEventsService, IframeOverlayService, ConfirmationService, - DotcmsEventsService, - DotEventsSocket, DotcmsConfigService, LoggerService, StringUtils, Title, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: DotHttpErrorManagerService, useValue: { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-create-contentlet/dot-create-contentlet.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-create-contentlet/dot-create-contentlet.component.spec.ts index 26f6704a5ff8..0692661feb90 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-create-contentlet/dot-create-contentlet.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-create-contentlet/dot-create-contentlet.component.spec.ts @@ -18,12 +18,8 @@ import { DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; -import { - DotcmsEventsServiceMock, - LoginServiceMock, - MockDotRouterService -} from '@dotcms/utils-testing'; +import { LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; +import { LoginServiceMock, MockDotRouterService } from '@dotcms/utils-testing'; import { DotCreateContentletComponent } from './dot-create-contentlet.component'; @@ -82,7 +78,6 @@ describe('DotCreateContentletComponent', () => { provide: DotRouterService, useClass: MockDotRouterService }, - { provide: DotcmsEventsService, useClass: DotcmsEventsServiceMock }, { provide: ActivatedRoute, useValue: { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts index a3024b1d5865..e449def80f8b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { of, Subject } from 'rxjs'; import { DebugElement } from '@angular/core'; @@ -13,7 +14,7 @@ import { DotUiColorsService, DotLoadingIndicatorService } from '@dotcms/data-access'; -import { LoginService, LoggerService, StringUtils, DotcmsEventsService } from '@dotcms/dotcms-js'; +import { LoginService, LoggerService, StringUtils } from '@dotcms/dotcms-js'; import { DotMessagePipe } from '@dotcms/ui'; import { LoginServiceMock, @@ -74,13 +75,6 @@ describe('DotReorderMenuComponent', () => { overlay: new Subject() } }, - { - provide: DotcmsEventsService, - useValue: { - subscribeToEvents: jest.fn().mockReturnValue(of({})), - subscribeTo: jest.fn().mockReturnValue(of({})) - } - }, { provide: LoggerService, useValue: { debug: jest.fn() } }, { provide: StringUtils, useValue: { to: jest.fn() } } ] diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-iframe-dialog/dot-iframe-dialog.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-iframe-dialog/dot-iframe-dialog.component.spec.ts index b979b809362a..1b0905fc6fe4 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-iframe-dialog/dot-iframe-dialog.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-iframe-dialog/dot-iframe-dialog.component.spec.ts @@ -11,14 +11,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { DotIframeService, DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - LoginService, - StringUtils -} from '@dotcms/dotcms-js'; +import { LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { DotLoadingIndicatorService } from '@dotcms/utils'; import { LoginServiceMock } from '@dotcms/utils-testing'; @@ -68,16 +61,6 @@ describe('DotIframeDialogComponent', () => { DotIframeService, DotRouterService, DotUiColorsService, - DotcmsEventsService, - DotEventsSocket, - { - provide: DotEventsSocketURL, - useFactory: () => - new DotEventsSocketURL( - `${typeof window !== 'undefined' ? window.location.hostname : ''}:${typeof window !== 'undefined' ? window.location.port : ''}/api/ws/v1/system/events`, - typeof window !== 'undefined' && window.location.protocol === 'https:' - ) - }, DotLoadingIndicatorService, LoggerService, StringUtils, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts index 8f226a4f2d1f..a80450ebc08e 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts @@ -12,12 +12,11 @@ import { RouterTestingModule } from '@angular/router/testing'; import { DotCurrentUserService, DotEventsService, - DotEventsSocket, DotIframeService, DotRouterService, DotSystemConfigService } from '@dotcms/data-access'; -import { Auth, DotcmsEventsService, LoginService } from '@dotcms/dotcms-js'; +import { Auth, LoginService } from '@dotcms/dotcms-js'; import { DotMenu } from '@dotcms/dotcms-models'; import { GlobalStore } from '@dotcms/store'; import { DotCurrentUserServiceMock, LoginServiceMock } from '@dotcms/utils-testing'; @@ -90,18 +89,6 @@ class TitleServiceMock { setTitle = jest.fn(); } -class DotcmsEventsServiceMock { - _events: Subject = new Subject(); - - subscribeTo() { - return this._events; - } - - trigger() { - this._events.next(); - } -} - export const dotMenuMock = () => { return { active: false, @@ -178,7 +165,6 @@ describe('DotNavigationService', () => { let service: DotNavigationService; let dotRouterService: DotRouterService; - let dotcmsEventsService: DotcmsEventsService; let dotEventService: DotEventsService; let dotMenuService: DotMenuService; let loginService: LoginService; @@ -223,10 +209,6 @@ describe('DotNavigationService', () => { providers: [ DotEventsService, DotNavigationService, - { - provide: DotcmsEventsService, - useClass: DotcmsEventsServiceMock - }, { provide: Title, useClass: TitleServiceMock @@ -267,7 +249,6 @@ describe('DotNavigationService', () => { { provide: DotCurrentUserService, useClass: DotCurrentUserServiceMock }, GlobalStore, { - provide: DotEventsSocket, useValue: { connect: jest.fn().mockReturnValue(of(null)), status$: jest.fn().mockReturnValue(of('connected')), @@ -283,7 +264,6 @@ describe('DotNavigationService', () => { service = TestBed.inject(DotNavigationService); dotRouterService = TestBed.inject(DotRouterService); - dotcmsEventsService = TestBed.inject(DotcmsEventsService); dotMenuService = TestBed.inject(DotMenuService); loginService = TestBed.inject(LoginService); dotEventService = TestBed.inject(DotEventsService); @@ -381,10 +361,4 @@ describe('DotNavigationService', () => { done(); }, 100); }); - - // TODO: needs to fix this, looks like the dotcmsEventsService instance is different here not sure why. - xit('should subscribe to UPDATE_PORTLET_LAYOUTS websocket event', () => { - expect(dotcmsEventsService.subscribeTo).toHaveBeenCalledWith('UPDATE_PORTLET_LAYOUTS'); - expect(dotcmsEventsService.subscribeTo).toHaveBeenCalledTimes(1); - }); }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts index 36c6b46e40c0..9d2aa25cdc4c 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts @@ -20,7 +20,7 @@ import { DotWorkflowActionsFireService, PaginatorService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoginService, SiteService } from '@dotcms/dotcms-js'; +import { LoginService, SiteService } from '@dotcms/dotcms-js'; import { DotPersona, DotSystemConfig } from '@dotcms/dotcms-models'; import { cleanUpDialog, @@ -126,11 +126,7 @@ describe('DotPersonaSelectorComponent', () => { DotWorkflowActionsFireService, ConfirmationService, DotAlertConfirmService, - DotEventsService, - { - provide: DotcmsEventsService, - useValue: { subscribeToEvents: jest.fn().mockReturnValue(EMPTY) } - } + DotEventsService ], detectChanges: false }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/components/dot-notification-item/dot-notification-item.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/components/dot-notification-item/dot-notification-item.component.spec.ts index 749c4cf30229..edee930e88ea 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/components/dot-notification-item/dot-notification-item.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-notifications/components/dot-notification-item/dot-notification-item.component.spec.ts @@ -4,25 +4,11 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { DotMessageService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - DotEventsSocketURL, - LoggerService, - StringUtils, - DotEventsSocket -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoggerService, StringUtils } from '@dotcms/dotcms-js'; import { MockDotMessageService } from '@dotcms/utils-testing'; import { DotNotificationItemComponent } from './dot-notification-item.component'; -export const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); -}; - describe('DotNotificationItemComponent', () => { let spectator: Spectator; let component: DotNotificationItemComponent; @@ -37,12 +23,9 @@ describe('DotNotificationItemComponent', () => { provideHttpClient(), provideHttpClientTesting(), { provide: DotMessageService, useValue: messageServiceMock }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, - DotcmsEventsService, DotcmsConfigService, LoggerService, - StringUtils, - DotEventsSocket + StringUtils ], detectChanges: false }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/store/dot-toolbar-user.store.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/store/dot-toolbar-user.store.spec.ts index 887354fe70fa..93830e72edcd 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/store/dot-toolbar-user.store.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/store/dot-toolbar-user.store.spec.ts @@ -13,15 +13,7 @@ import { DotSystemConfigService, DotIframeService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - LoginService, - StringUtils -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { GlobalStore } from '@dotcms/store'; import { DotCurrentUserServiceMock, LoginServiceMock, mockAuth } from '@dotcms/utils-testing'; @@ -29,7 +21,6 @@ import { DotToolbarUserStore } from './dot-toolbar-user.store'; import { DotMenuService } from '../../../../../../api/services/dot-menu.service'; import { LOCATION_TOKEN } from '../../../../../../providers'; -import { dotEventSocketURLFactory } from '../../../../../../test/dot-test-bed'; import { DotNavigationService } from '../../../../dot-navigation/services/dot-navigation.service'; describe('DotToolbarUserStore', () => { @@ -49,8 +40,6 @@ describe('DotToolbarUserStore', () => { DotEventsService, DotIframeService, DotMenuService, - DotcmsEventsService, - DotEventsSocket, DotcmsConfigService, StringUtils, DotRouterService, @@ -63,7 +52,6 @@ describe('DotToolbarUserStore', () => { } } }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: LoginService, useClass: LoginServiceMock }, { provide: DotSystemConfigService, diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-workflow-task-detail/dot-workflow-task-detail.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-workflow-task-detail/dot-workflow-task-detail.component.spec.ts index d66e1f0a0f86..2d3c91f4ab9b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-workflow-task-detail/dot-workflow-task-detail.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-workflow-task-detail/dot-workflow-task-detail.component.spec.ts @@ -6,22 +6,13 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ActivatedRoute } from '@angular/router'; import { DotIframeService, DotRouterService, DotUiColorsService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, - LoggerService, - LoginService, - StringUtils -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoggerService, LoginService, StringUtils } from '@dotcms/dotcms-js'; import { LoginServiceMock, MockDotRouterService } from '@dotcms/utils-testing'; import { DotWorkflowTaskDetailComponent } from './dot-workflow-task-detail.component'; import { DotWorkflowTaskDetailService } from './services/dot-workflow-task-detail.service'; import { DotMenuService } from '../../../api/services/dot-menu.service'; -import { dotEventSocketURLFactory } from '../../../test/dot-test-bed'; import { IframeOverlayService } from '../_common/iframe/service/iframe-overlay.service'; import { DotIframeDialogComponent } from '../dot-iframe-dialog/dot-iframe-dialog.component'; @@ -39,12 +30,9 @@ describe('DotWorkflowTaskDetailComponent', () => { DotIframeService, DotUiColorsService, IframeOverlayService, - DotcmsEventsService, - DotEventsSocket, DotcmsConfigService, LoggerService, StringUtils, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: LoginService, useClass: LoginServiceMock }, { provide: DotRouterService, useClass: MockDotRouterService }, { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts index 9f2cbc5f1dbb..efeae12dfbf2 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts @@ -1,3 +1,4 @@ +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { mockProvider } from '@ngneat/spectator/jest'; @@ -35,9 +36,6 @@ import { import { ApiRoot, DotcmsConfigService, - DotcmsEventsService, - DotEventsSocket, - DotEventsSocketURL, DotPushPublishDialogService, LoggerService, LoginService, @@ -54,7 +52,6 @@ import { DotDownloadBundleDialogService } from '../../../api/services/dot-downlo import { DotMenuService } from '../../../api/services/dot-menu.service'; import { NotificationsService } from '../../../api/services/notifications-service'; import { LOCATION_TOKEN } from '../../../providers'; -import { dotEventSocketURLFactory, MockDotUiColorsService } from '../../../test/dot-test-bed'; import { DotDownloadBundleDialogComponent } from '../_common/dot-download-bundle-dialog/dot-download-bundle-dialog.component'; import { DotWizardComponent } from '../_common/dot-wizard/dot-wizard.component'; import { IframeOverlayService } from '../_common/iframe/service/iframe-overlay.service'; @@ -145,9 +142,6 @@ describe('MainLegacyComponent', () => { DotFormatDateService, DotAlertConfirmService, ConfirmationService, - DotcmsEventsService, - DotEventsSocket, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, DotcmsConfigService, LoggerService, StringUtils, diff --git a/core-web/libs/data-access/src/lib/dot-workflow-event-handler/dot-workflow-event-handler.service.spec.ts b/core-web/libs/data-access/src/lib/dot-workflow-event-handler/dot-workflow-event-handler.service.spec.ts index 40db3a97e78a..8750da6e8141 100644 --- a/core-web/libs/data-access/src/lib/dot-workflow-event-handler/dot-workflow-event-handler.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-workflow-event-handler/dot-workflow-event-handler.service.spec.ts @@ -3,7 +3,7 @@ import { of } from 'rxjs'; import { Injectable } from '@angular/core'; -import { DotEventsSocketURL, LoginService } from '@dotcms/dotcms-js'; +import { LoginService } from '@dotcms/dotcms-js'; import { DotActionBulkRequestOptions, DotActionBulkResult, @@ -37,13 +37,6 @@ import { DotWizardService } from '../dot-wizard/dot-wizard.service'; import { DotWorkflowActionsFireService } from '../dot-workflow-actions-fire/dot-workflow-actions-fire.service'; import { PushPublishService } from '../push-publish/push-publish.service'; -const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); -}; - @Injectable() export class MockPushPublishService { getEnvironments() { @@ -155,7 +148,6 @@ describe('DotWorkflowEventHandlerService', () => { provide: LoginService, useClass: LoginServiceMock }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: DotFormatDateService, useClass: DotFormatDateServiceMock } ] }); diff --git a/core-web/libs/dot-rules/src/lib/rule-engine.module.ts b/core-web/libs/dot-rules/src/lib/rule-engine.module.ts index 4fd15f8f16e8..fb0e4ed4b4d3 100644 --- a/core-web/libs/dot-rules/src/lib/rule-engine.module.ts +++ b/core-web/libs/dot-rules/src/lib/rule-engine.module.ts @@ -5,7 +5,6 @@ import { ApiRoot, BrowserUtil, DotcmsConfigService, - DotcmsEventsService, LoggerService, StringUtils, UserModel @@ -49,7 +48,6 @@ import { RuleViewService } from './services/ui/dot-view-rule-service'; ApiRoot, BrowserUtil, DotcmsConfigService, - DotcmsEventsService, LoggerService, StringUtils, UserModel, diff --git a/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.spec.ts b/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.spec.ts deleted file mode 100644 index 172baee4376f..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.spec.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Observable, Subject, of } from 'rxjs'; - -import { ReflectiveInjector } from '@angular/core'; - -import { DotcmsEventsService } from './dotcms-events.service'; -import { LoggerService } from './logger.service'; -import { DotEventTypeWrapper } from './models'; -import { StringUtils } from './string-utils.service'; -import { DotEventsSocket } from './util/dot-event-socket'; -import { DotEventMessage } from './util/models/dot-event-message'; - -class DotEventsSocketMock { - _messages: Subject = new Subject(); - _open: Subject = new Subject(); - private connected = false; - - connect(): Observable { - this.connected = true; - - return of(true); - } - - open(): Observable { - return this._open.asObservable(); - } - - messages(): Observable { - return this._messages.asObservable(); - } - - public sendMessage(message: DotEventMessage) { - this._messages.next(message); - } - - isConnected(): boolean { - return this.connected; - } - - destroy(): void {} -} - -describe('DotcmsEventsService', () => { - let socket: DotEventsSocketMock; - let dotcmsEventsService: DotcmsEventsService; - - let injector: ReflectiveInjector; - - beforeEach(() => { - socket = new DotEventsSocketMock(); - - injector = ReflectiveInjector.resolveAndCreate([ - { provide: DotEventsSocket, useValue: socket }, - StringUtils, - LoggerService, - DotcmsEventsService - ]); - - dotcmsEventsService = injector.get(DotcmsEventsService); - - spyOn(socket, 'connect').and.callThrough(); - spyOn(socket, 'destroy').and.callThrough(); - }); - - it('should create and connect a new socket', () => { - dotcmsEventsService.start(); - - expect(socket.connect).toHaveBeenCalled(); - }); - - it('should reuse socket', () => { - dotcmsEventsService.start(); - dotcmsEventsService.start(); - - expect(socket.connect).toHaveBeenCalledTimes(1); - }); - - it('should trigger open event', (done) => { - dotcmsEventsService.open().subscribe(() => { - done(); - }); - - socket._open.next(true); - }); - - it('should subscribe to a event', (done) => { - dotcmsEventsService.start(); - - dotcmsEventsService.subscribeTo('test_event').subscribe((dotEventData: any) => { - expect(dotEventData).toEqual('test payload'); - done(); - }); - - socket.sendMessage({ - event: 'test_event', - payload: { - data: 'test payload' - } - }); - }); - - it('should subscribe to several events', () => { - let count = 0; - - dotcmsEventsService.start(); - - dotcmsEventsService - .subscribeToEvents(['test_event_1', 'test_event_2']) - .subscribe((dotEventTypeWrapper: DotEventTypeWrapper) => { - if (dotEventTypeWrapper.name === 'test_event_1') { - expect(dotEventTypeWrapper.data).toEqual('test payload_1'); - } else if (dotEventTypeWrapper.name === 'test_event_2') { - expect(dotEventTypeWrapper.data).toEqual('test payload_2'); - } else { - expect(true).toBe(false); - } - - count++; - }); - - socket.sendMessage({ - event: 'test_event_1', - payload: { - data: 'test payload_1' - } - }); - - socket.sendMessage({ - event: 'test_event_2', - payload: { - data: 'test payload_2' - } - }); - - expect(count).toBe(2); - }); - - it('should destroy socket', () => { - dotcmsEventsService.start(); - dotcmsEventsService.destroy(); - - expect(socket.destroy).toHaveBeenCalled(); - }); - - it('should destroy socket and connect', () => { - dotcmsEventsService.start(); - dotcmsEventsService.destroy(); - expect(socket.destroy).toHaveBeenCalled(); - - dotcmsEventsService.start(); - expect(socket.connect).toHaveBeenCalledTimes(1); - }); - - it('should stop emitting after destroy', () => { - const subscribeCallback = jasmine.createSpy('spy'); - - dotcmsEventsService.start(); - dotcmsEventsService.subscribeTo('test_event').subscribe(subscribeCallback); - dotcmsEventsService.destroy(); - - socket.sendMessage({ - event: 'test_event', - payload: 'test payload' - }); - - expect(subscribeCallback).not.toHaveBeenCalled(); - }); -}); diff --git a/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts b/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts deleted file mode 100644 index 697ce4e6a1ed..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Observable, Subscription, Subject } from 'rxjs'; - -import { Injectable, inject } from '@angular/core'; - -import { switchMap } from 'rxjs/operators'; - -import { LoggerService } from './logger.service'; -import { DotEventTypeWrapper } from './models'; -import { DotEventsSocket } from './util/dot-event-socket'; -import { DotEventMessage } from './util/models/dot-event-message'; - -@Injectable() -export class DotcmsEventsService { - private dotEventsSocket = inject(DotEventsSocket); - private loggerService = inject(LoggerService); - - private subjects = []; - private messagesSub: Subscription; - - /** - * Close the socket - * - * @memberof DotcmsEventsService - */ - destroy(): void { - this.dotEventsSocket.destroy(); - this.messagesSub.unsubscribe(); - } - - /** - * Start the socket - * - * @memberof DotcmsEventsService - */ - start(): void { - this.loggerService.debug('start DotcmsEventsService', this.dotEventsSocket.isConnected()); - if (!this.dotEventsSocket.isConnected()) { - this.loggerService.debug('Connecting with socket'); - - this.messagesSub = this.dotEventsSocket - .connect() - .pipe(switchMap(() => this.dotEventsSocket.messages())) - .subscribe( - ({ event, payload }: DotEventMessage) => { - if (!this.subjects[event]) { - this.subjects[event] = new Subject(); - } - - this.subjects[event].next(payload.data); - }, - (e) => { - this.loggerService.debug( - 'Error in the System Events service: ' + e.message - ); - }, - () => { - this.loggerService.debug('Completed'); - } - ); - } - } - - /** - * This method will be called by clients that want to receive notifications - * regarding incoming system events. The events they will receive will be - * based on the type of event clients register for. - * - * @memberof DotcmsEventsService - */ - subscribeTo(clientEventType: string): Observable { - if (!this.subjects[clientEventType]) { - this.subjects[clientEventType] = new Subject(); - } - - return this.subjects[clientEventType].asObservable(); - } - - /** - * Subscribe to multiple events from the DotCMS WebSocket - * - * @memberof DotcmsEventsService - */ - subscribeToEvents(clientEventTypes: string[]): Observable> { - const subject: Subject> = new Subject>(); - - clientEventTypes.forEach((eventType: string) => { - this.subscribeTo(eventType).subscribe((data: T) => { - subject.next({ - data: data, - name: eventType - }); - }); - }); - - return subject.asObservable(); - } - - /** - * Listen when the socket is opened - * - * @returns Observable - * @memberof DotcmsEventsService - */ - open(): Observable { - return this.dotEventsSocket.open(); - } -} diff --git a/core-web/libs/dotcms-js/src/lib/core/login.service.ts b/core-web/libs/dotcms-js/src/lib/core/login.service.ts index 6230ece2e740..978401269bee 100644 --- a/core-web/libs/dotcms-js/src/lib/core/login.service.ts +++ b/core-web/libs/dotcms-js/src/lib/core/login.service.ts @@ -3,17 +3,17 @@ import { Observable, of, Subject } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { map, tap } from 'rxjs/operators'; +import { DotEventsSocket } from '@dotcms/data-access'; import { DotCMSResponse, DotLoginInformation, SESSION_STORAGE_VARIATION_KEY } from '@dotcms/dotcms-models'; -import { DotcmsEventsService } from './dotcms-events.service'; - export interface DotLoginParams { login: string; password: string; @@ -33,7 +33,7 @@ export const LOGOUT_URL = '/dotAdmin/logout'; }) export class LoginService { private http = inject(HttpClient); - private dotcmsEventsService = inject(DotcmsEventsService); + private eventsSocket = inject(DotEventsSocket); currentUserLanguageId = ''; private country = ''; @@ -41,8 +41,6 @@ export class LoginService { private urls: Record; constructor() { - const dotcmsEventsService = this.dotcmsEventsService; - this._loginAsUsersList$ = new Subject(); this.urls = { @@ -57,15 +55,20 @@ export class LoginService { current: '/api/v1/users/current/' }; - // when the session is expired/destroyed - dotcmsEventsService.subscribeTo('SESSION_DESTROYED').subscribe(() => { - this.logOutUser(); - this.clearExperimentPersistence(); - }); - - dotcmsEventsService.subscribeTo('SESSION_LOGOUT').subscribe(() => { - this.clearExperimentPersistence(); - }); + this.eventsSocket + .on('SESSION_DESTROYED') + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this.logOutUser(); + this.clearExperimentPersistence(); + }); + + this.eventsSocket + .on('SESSION_LOGOUT') + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this.clearExperimentPersistence(); + }); } private _auth$: Subject = new Subject(); @@ -301,8 +304,6 @@ export class LoginService { // When not logged user we need to fire the observable chain if (!auth.user) { this._logout$.next(); - } else { - this.dotcmsEventsService.start(); } } diff --git a/core-web/libs/dotcms-js/src/lib/core/routing.service.ts b/core-web/libs/dotcms-js/src/lib/core/routing.service.ts index 037fb990323c..24d94604a64b 100644 --- a/core-web/libs/dotcms-js/src/lib/core/routing.service.ts +++ b/core-web/libs/dotcms-js/src/lib/core/routing.service.ts @@ -2,13 +2,14 @@ import { Observable, Subject } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { map } from 'rxjs/operators'; +import { DotEventsSocket } from '@dotcms/data-access'; import { DotCMSResponse } from '@dotcms/dotcms-models'; import { DotRouterService } from './dot-router.service'; -import { DotcmsEventsService } from './dotcms-events.service'; import { LoginService } from './login.service'; @Injectable() @@ -28,15 +29,16 @@ export class RoutingService { // TODO: I think we should be able to remove the routing injection constructor() { const loginService = inject(LoginService); - const dotcmsEventsService = inject(DotcmsEventsService); + const eventsSocket = inject(DotEventsSocket); this.urlMenus = '/api/v1/CORE_WEB/menu'; this.portlets = new Map(); loginService.watchUser(this.loadMenus.bind(this)); - dotcmsEventsService - .subscribeTo('UPDATE_PORTLET_LAYOUTS') - .subscribe(this.loadMenus.bind(this)); + eventsSocket + .on('UPDATE_PORTLET_LAYOUTS') + .pipe(takeUntilDestroyed()) + .subscribe(() => this.loadMenus()); } get currentPortletId(): string { diff --git a/core-web/libs/dotcms-js/src/lib/core/site.service.ts b/core-web/libs/dotcms-js/src/lib/core/site.service.ts index 7dbdfe0acc13..ba41cee5a0c0 100644 --- a/core-web/libs/dotcms-js/src/lib/core/site.service.ts +++ b/core-web/libs/dotcms-js/src/lib/core/site.service.ts @@ -7,7 +7,6 @@ import { filter, map, skip, startWith, switchMap, take, tap } from 'rxjs/operato import { DotCMSResponse } from '@dotcms/dotcms-models'; -import { DotcmsEventsService } from './dotcms-events.service'; import { LoggerService } from './logger.service'; import { LoginService } from './login.service'; import { DotEventTypeWrapper } from './models/dot-events/dot-event-type-wrapper'; @@ -48,7 +47,6 @@ export class SiteService { constructor() { const loginService = inject(LoginService); - const dotcmsEventsService = inject(DotcmsEventsService); this.urls = { currentSiteUrl: '/api/v1/site/currentSite', @@ -56,18 +54,6 @@ export class SiteService { switchSiteUrl: '/api/v1/site/switch' }; - dotcmsEventsService - .subscribeToEvents(['ARCHIVE_SITE', 'UPDATE_SITE']) - .subscribe((event: DotEventTypeWrapper) => this.eventResponse(event)); - - dotcmsEventsService - .subscribeToEvents(this.events) - .subscribe(({ data }: DotEventTypeWrapper) => this.siteEventsHandler(data)); - - dotcmsEventsService - .subscribeToEvents(['SWITCH_SITE']) - .subscribe(({ data }: DotEventTypeWrapper) => this.setCurrentSite(data)); - loginService.watchUser(() => this.loadCurrentSite()); } diff --git a/core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.spec.ts b/core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.spec.ts deleted file mode 100644 index 354b0e460fa5..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Server } from 'mock-socket'; -import { Observable, of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { DotEventsSocket } from './dot-event-socket'; -import { DotEventMessage } from './models/dot-event-message'; -import { DotEventsSocketURL } from './models/dot-event-socket-url'; - -import { ConfigParams, DotcmsConfigService } from '../dotcms-config.service'; -import { LoggerService } from '../logger.service'; -import { StringUtils } from '../string-utils.service'; - -class DotcmsConfigMock { - getConfig(): Observable { - return of({ - colors: {}, - emailRegex: '', - license: {}, - menu: [], - paginatorLinks: 1, - paginatorRows: 2, - websocket: { - websocketReconnectTime: 0, - disabledWebsockets: false - } - }); - } -} - -describe('DotEventsSocket', () => { - let dotEventsSocket: DotEventsSocket; - let httpTesting: HttpTestingController; - const url = new DotEventsSocketURL('localhost/testing', false); - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - { provide: DotcmsConfigService, useClass: DotcmsConfigMock }, - { provide: DotEventsSocketURL, useValue: url }, - StringUtils, - LoggerService, - DotEventsSocket - ] - }); - - dotEventsSocket = TestBed.inject(DotEventsSocket); - httpTesting = TestBed.inject(HttpTestingController); - }); - - describe('WebSocket', () => { - let mockwebSocketServer: Server; - - beforeEach(() => { - mockwebSocketServer = new Server(url.getWebSocketURL()); - }); - - it('should connect', (done) => { - mockwebSocketServer.on('connection', () => { - done(); - }); - - dotEventsSocket.connect().subscribe(() => {}); - }); - - it('should catch a message', (done) => { - const expectedMessage: DotEventMessage = { - event: 'event', - payload: 'message' - }; - - mockwebSocketServer.on('connection', (socket) => { - socket.send(JSON.stringify(expectedMessage)); - done(); - }); - - dotEventsSocket.messages().subscribe((message) => { - expect(message).toEqual(expectedMessage); - }); - dotEventsSocket.connect().subscribe(() => {}); - }); - - afterEach(() => { - mockwebSocketServer.close(); - }); - }); - - describe('LongPolling', () => { - const longPollingUrl = 'http://localhost/testing'; - - it('should connect', (done) => { - dotEventsSocket.connect().subscribe(() => {}); - - dotEventsSocket.open().subscribe(() => { - done(); - }); - - const req = httpTesting.expectOne(longPollingUrl); - dotEventsSocket.destroy(); - req.flush({ entity: { message: 'message' } }); - }); - - it('should catch a message', (done) => { - dotEventsSocket.connect().subscribe(() => {}); - - dotEventsSocket.messages().subscribe((message) => { - dotEventsSocket.destroy(); - expect(message).toEqual({ - event: 'event', - payload: 'message' - }); - done(); - }); - - const req = httpTesting.expectOne(longPollingUrl); - req.flush({ entity: { event: 'event', payload: 'message' } }); - }); - - it('should fallback to long polling after websocket error and catch message', (done) => { - dotEventsSocket.connect().subscribe(() => {}); - - dotEventsSocket.messages().subscribe((message) => { - dotEventsSocket.destroy(); - expect(message).toEqual({ - event: 'event', - payload: 'message' - }); - done(); - }); - - const req1 = httpTesting.expectOne(longPollingUrl); - req1.flush(null, { status: 500, statusText: 'Server Error' }); - - const req2 = httpTesting.expectOne(longPollingUrl); - req2.flush({ entity: { event: 'event', payload: 'message' } }); - }); - }); -}); diff --git a/core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.ts b/core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.ts deleted file mode 100644 index 7bd04d7dbe7a..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/util/dot-event-socket.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { Subject, Observable, timer } from 'rxjs'; - -import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; - -import { tap } from 'rxjs/operators'; - -import { LongPollingProtocol } from './long-polling-protocol'; -import { DotEventMessage } from './models/dot-event-message'; -import { DotEventsSocketURL } from './models/dot-event-socket-url'; -import { Protocol } from './protocol'; -import { WebSocketProtocol } from './websockets-protocol'; - -import { ConfigParams, DotcmsConfigService, WebSocketConfigParams } from '../dotcms-config.service'; -import { LoggerService } from '../logger.service'; - -enum ConnectionStatus { - NONE, - CONNECTING, - RECONNECTING, - CONNECTED, - CLOSED -} - -/** - * It is a socket to receive notifications when a event is triggered by the server, first this try to establish a web socket - * connection if it fails then try a Long polling connection instead. - * - * If the connection is lost at any point it will try to reconnect automatically after a time set by configuration parameters. - * It implements an exponential backoff strategy with jitter for reconnection attempts. - * - * @export - */ -@Injectable() -export class DotEventsSocket { - private dotEventsSocketURL = inject(DotEventsSocketURL); - private dotcmsConfigService = inject(DotcmsConfigService); - private loggerService = inject(LoggerService); - private http = inject(HttpClient); - - private protocolImpl: Protocol; - - private status: ConnectionStatus = ConnectionStatus.NONE; - private _message: Subject = new Subject(); - private _open: Subject = new Subject(); - private webSocketConfigParams: WebSocketConfigParams; - private readonly MAX_RETRIES = 100000; - private readonly INITIAL_RETRY_DELAY = 1000; - private readonly MAX_RETRY_DELAY = 30000; // 30 seconds max delay - private retryCount = 0; - - /** - * Connect to a Event socket using Web Socket protocol, - * if a Web Socket connection can be stablish then try again with a Long Polling connection. - * - * @returns Observable - * @memberof DotEventsSocket - */ - connect(): Observable { - // Using the init method and making sure the return type is correct - return this.dotcmsConfigService.getConfig().pipe( - tap((config) => { - this.webSocketConfigParams = config.websocket; - this.protocolImpl = - this.isWebSocketsBrowserSupport() && - !this.webSocketConfigParams.disabledWebsockets - ? this.getWebSocketProtocol() - : this.getLongPollingProtocol(); - this.status = ConnectionStatus.CONNECTING; - this.connectProtocol(); - }) - ); - } - - /** - * Destroy the Event socket - * - * @memberof DotEventsSocket - */ - destroy(): void { - // On logout, meaning no authenticated user lets try to close the socket - if (this.protocolImpl) { - this.loggerService.debug('Closing socket'); - this.status = ConnectionStatus.CLOSED; - this.protocolImpl.close(); - } - } - - /** - * Trigger when a message is received - * - * @returns Observable - * @memberof DotEventsSocket - */ - messages(): Observable { - return this._message.asObservable(); - } - - /** - * Trigger when a connect is open - * - * @returns Observable - * @memberof DotEventsSocket - */ - open(): Observable { - return this._open.asObservable(); - } - - /** - * Return true if the socket is connected otherwise return false - * - * @returns boolean - * @memberof DotEventsSocket - */ - isConnected(): boolean { - return this.status === ConnectionStatus.CONNECTED; - } - - private connectProtocol(): void { - this.protocolImpl.open$().subscribe(() => { - this.status = ConnectionStatus.CONNECTED; - this._open.next(true); - // Reset retry counter on successful connection - this.retryCount = 0; - }); - - this.protocolImpl.error$().subscribe(() => { - if (this.shouldTryWithLongPooling()) { - this.loggerService.info( - 'Error connecting with Websockets, trying again with long polling' - ); - - this.protocolImpl.destroy(); - this.protocolImpl = this.getLongPollingProtocol(); - this.connectProtocol(); - } else { - this.reconnect(); - } - }); - - this.protocolImpl.close$().subscribe((_event) => { - if (this.status !== ConnectionStatus.CLOSED) { - this.loggerService.info('Connection closed unexpectedly, attempting to reconnect'); - this.reconnect(); - } else { - this.loggerService.debug('Connection closed normally'); - } - }); - - this.protocolImpl.message$().subscribe( - (res) => this._message.next(res), - (e) => { - this.loggerService.debug('Error in the System Events service: ' + e.message); - this.reconnect(); - }, - () => this.loggerService.debug('Completed') - ); - - this.protocolImpl.connect(); - } - - private reconnect(): void { - // Don't attempt more reconnections than MAX_RETRIES (which is a big number because we always want to reconnect) - if (this.retryCount >= this.MAX_RETRIES) { - this.loggerService.info( - `Maximum reconnection attempts (${this.MAX_RETRIES}) reached. Giving up.` - ); - this.status = ConnectionStatus.CLOSED; - - return; - } - - this.status = this.getAfterErrorStatus(); - this.retryCount++; - - const delay = this.calculateReconnectDelay(); - - this.loggerService.info( - `Scheduling reconnection attempt ${this.retryCount}/${this.MAX_RETRIES} in ${delay}ms` - ); - - timer(delay).subscribe(() => { - if (this.status !== ConnectionStatus.CLOSED) { - this.loggerService.info('Attempting to reconnect'); - this.protocolImpl.connect(); - } - }); - } - - private calculateReconnectDelay(): number { - // Use configured time or default delay with a random jitter to prevent thundering herd - const baseDelay = - this.webSocketConfigParams?.websocketReconnectTime || this.INITIAL_RETRY_DELAY; - - // Exponential backoff with jitter, capped at MAX_RETRY_DELAY - const exponentialDelay = Math.min( - baseDelay * Math.pow(2, Math.min(this.retryCount, 10)), // Cap exponential growth - this.MAX_RETRY_DELAY - ); - - // Add random jitter to prevent reconnection thundering herd problems - const jitter = Math.random() * 1000; // Up to 1 second of jitter - - return exponentialDelay + jitter; - } - - private getAfterErrorStatus(): ConnectionStatus { - return this.status === ConnectionStatus.CONNECTING - ? ConnectionStatus.CONNECTING - : ConnectionStatus.RECONNECTING; - } - - private shouldTryWithLongPooling(): boolean { - return ( - this.isWebSocketProtocol() && - this.status !== ConnectionStatus.CONNECTED && - this.status !== ConnectionStatus.RECONNECTING - ); - } - - private getWebSocketProtocol(): WebSocketProtocol { - return new WebSocketProtocol(this.dotEventsSocketURL.getWebSocketURL(), this.loggerService); - } - - private getLongPollingProtocol(): LongPollingProtocol { - return new LongPollingProtocol( - this.dotEventsSocketURL.getLongPoolingURL(), - this.loggerService, - this.http - ); - } - - private isWebSocketsBrowserSupport(): boolean { - return 'WebSocket' in window || 'MozWebSocket' in window; - } - - private isWebSocketProtocol(): boolean { - return this.protocolImpl instanceof WebSocketProtocol; - } -} diff --git a/core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.spec.ts b/core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.spec.ts deleted file mode 100644 index ad1d91b6d6c5..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { of, throwError } from 'rxjs'; - -import { HttpClient } from '@angular/common/http'; -import { provideHttpClient } from '@angular/common/http'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { LongPollingProtocol } from './long-polling-protocol'; - -import { LoggerService } from '../logger.service'; -import { StringUtils } from '../string-utils.service'; - -describe('LongPollingProtocol', () => { - let httpClient: HttpClient; - let httpTesting: HttpTestingController; - let longPollingProtocol: LongPollingProtocol; - const url = 'http://testing'; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideHttpClient(), provideHttpClientTesting(), StringUtils, LoggerService] - }); - - httpClient = TestBed.inject(HttpClient); - httpTesting = TestBed.inject(HttpTestingController); - - const loggerService = TestBed.inject(LoggerService); - longPollingProtocol = new LongPollingProtocol(url, loggerService, httpClient); - }); - - afterEach(() => { - httpTesting.verify(); - }); - - it('should connect', () => { - longPollingProtocol.connect(); - - const req = httpTesting.expectOne(url); - expect(req.request.method).toBe('GET'); - - longPollingProtocol.close(); - req.flush({ entity: { message: 'message' } }); - }); - - it('should trigger message', (done) => { - longPollingProtocol.message$().subscribe((message) => { - expect(message).toEqual({ message: 'message' }); - done(); - }); - - longPollingProtocol.connect(); - - const req = httpTesting.expectOne(url); - longPollingProtocol.close(); - req.flush({ entity: { message: 'message' } }); - }); - - it('should trigger message with lastCallback', (done) => { - let countRequest = 0; - - longPollingProtocol.message$().subscribe((message) => { - expect(message).toEqual({ message: 'message', creationDate: 1 }); - countRequest++; - - if (countRequest === 2) { - longPollingProtocol.close(); - done(); - } - }); - - longPollingProtocol.connect(); - - const req1 = httpTesting.expectOne(url); - req1.flush({ entity: [{ message: 'message', creationDate: 1 }] }); - - const req2 = httpTesting.expectOne(`${url}?lastCallBack=2`); - longPollingProtocol.close(); - req2.flush({ entity: [{ message: 'message', creationDate: 1 }] }); - }); - - it('should reconnect after a message', () => { - longPollingProtocol.connect(); - - const req1 = httpTesting.expectOne(url); - req1.flush({ entity: [{ message: 'message' }] }); - - const req2 = httpTesting.expectOne(url); - longPollingProtocol.close(); - req2.flush({ entity: [{ message: 'message' }] }); - }); - - it('should trigger a error', (done) => { - longPollingProtocol.error$().subscribe(() => { - done(); - }); - - longPollingProtocol.connect(); - - const req = httpTesting.expectOne(url); - req.flush(null, { status: 500, statusText: 'Server Error' }); - }); -}); diff --git a/core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.ts b/core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.ts deleted file mode 100644 index c2d9449d2d85..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/util/long-polling-protocol.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { HttpClient, HttpParams } from '@angular/common/http'; - -import { map, take } from 'rxjs/operators'; - -import { DotCMSResponse } from '@dotcms/dotcms-models'; - -import { Protocol } from './protocol'; - -import { LoggerService } from '../logger.service'; - -export class LongPollingProtocol extends Protocol { - private isClosed = false; - private isAlreadyOpen = false; - private lastCallback: number; - - constructor( - private url: string, - loggerService: LoggerService, - private http: HttpClient - ) { - super(loggerService); - } - - /** - * Connect to a Long Polling connection - */ - connect(): void { - this.connectLongPooling(); - } - - /** - * Close the connection - */ - close(): void { - this.loggerService.info('destroying long polling'); - this.isClosed = true; - this.isAlreadyOpen = false; - this._close.next(); - } - - private getLastCallback(data): number { - this.lastCallback = - data.length > 0 ? data[data.length - 1].creationDate + 1 : this.lastCallback; - - return this.lastCallback; - } - - private connectLongPooling(lastCallBack?: number): void { - this.isClosed = false; - this.loggerService.info('Starting long polling connection'); - - let params = new HttpParams(); - if (lastCallBack) { - params = params.set('lastCallBack', lastCallBack); - } - - this.http - .get(this.url, { params }) - .pipe( - map((res) => res.entity), - take(1) - ) - .subscribe( - (data) => { - this.loggerService.debug('new Events', data); - this.triggerOpen(); - - if (data instanceof Array) { - data.forEach((message) => { - this._message.next(message); - }); - } else { - this._message.next(data); - } - - if (!this.isClosed) { - this.connectLongPooling(this.getLastCallback(data)); - } - }, - (e) => { - this.loggerService.info('A error occur connecting through long polling'); - this._error.next(e); - } - ); - } - - private triggerOpen(): void { - if (!this.isAlreadyOpen) { - this._open.next(true); - this.isAlreadyOpen = true; - } - } -} diff --git a/core-web/libs/dotcms-js/src/lib/core/util/models/dot-event-socket-url.ts b/core-web/libs/dotcms-js/src/lib/core/util/models/dot-event-socket-url.ts deleted file mode 100644 index 9bcdb02e873d..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/util/models/dot-event-socket-url.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Represent a url to connect with a evend end point - * - * @export - */ -export class DotEventsSocketURL { - constructor( - private url: string, - private useSSL: boolean - ) {} - - /** - * Return the web socket url to connect with the Event end point - * - * @returns string - * @memberof DotEventsSocketURL - */ - public getWebSocketURL(): string { - return `${this.getWebSocketProtocol()}://${this.url}`; - } - - /** - * Return the long polling url to connect with the Event end point - * - * @returns string - * @memberof DotEventsSocketURL - */ - public getLongPoolingURL(): string { - return `${this.getHttpProtocol()}://${this.url}`; - } - - private getWebSocketProtocol(): string { - return `${this.useSSL ? 'wss' : 'ws'}`; - } - - private getHttpProtocol(): string { - return `${this.useSSL ? 'https' : 'http'}`; - } -} diff --git a/core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.spec.ts b/core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.spec.ts deleted file mode 100644 index a929b3187270..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Server } from 'mock-socket'; - -import { WebSocketProtocol } from './websockets-protocol'; - -import { LoggerService } from '../logger.service'; -import { StringUtils } from '../string-utils.service'; - -describe('WebSocketProtocol', () => { - let webSocketProtocol: WebSocketProtocol; - const url = 'wss://testing'; - let mockServer: Server; - - beforeEach(() => { - const loggerService = new LoggerService(new StringUtils()); - webSocketProtocol = new WebSocketProtocol(url, loggerService); - }); - - beforeEach(() => { - mockServer = new Server(url); - }); - - it('should connect and tigger open event', (done) => { - webSocketProtocol.open$().subscribe(() => { - done(); - }); - - webSocketProtocol.connect(); - }); - - it('should tigger message event', (done) => { - mockServer.on('connection', (socket) => { - socket.send( - JSON.stringify({ - data: 'testing' - }) - ); - }); - - webSocketProtocol.message$().subscribe((message) => { - expect(message).toEqual({ - data: 'testing' - }); - done(); - }); - - webSocketProtocol.connect(); - }); - - it('should tigger close event', (done) => { - webSocketProtocol.open$().subscribe(() => { - mockServer.close(); - }); - - webSocketProtocol.close$().subscribe(() => { - done(); - }); - - webSocketProtocol.error$().subscribe(() => { - expect(true).toBe(false, 'Should not trigger error event'); - }); - - webSocketProtocol.connect(); - }); - - afterEach(() => { - mockServer.close(); - }); -}); diff --git a/core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.ts b/core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.ts deleted file mode 100644 index bcb4f5fab68b..000000000000 --- a/core-web/libs/dotcms-js/src/lib/core/util/websockets-protocol.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Subject } from 'rxjs'; - -import { Protocol } from './protocol'; - -import { LoggerService } from '../logger.service'; - -enum WEB_SOCKET_PROTOCOL_CODE { - NORMAL_CLOSE_CODE = 1000, - GO_AWAY_CODE = 1001 -} -export class WebSocketProtocol extends Protocol { - dataStream: Subject<{}> = new Subject(); - private socket: WebSocket; - private errorThrown: boolean; - - constructor( - private url: string, - loggerService: LoggerService - ) { - super(loggerService); - - const match = new RegExp('wss?://').test(url); - if (!match) { - throw new Error('Invalid url provided [' + url + ']'); - } - } - - connect(): void { - this.errorThrown = false; - this.loggerService.debug('Connecting with Web socket', this.url); - - try { - this.socket = new WebSocket(this.url); - - this.socket.onopen = (ev: Event) => { - this.loggerService.debug('Web socket connected', this.url); - this._open.next(ev); - }; - - this.socket.onmessage = (ev: MessageEvent) => { - this._message.next(JSON.parse(ev.data)); - }; - - this.socket.onclose = (ev: CloseEvent) => { - if (!this.errorThrown) { - if (ev.code === WEB_SOCKET_PROTOCOL_CODE.NORMAL_CLOSE_CODE) { - this._close.next(); - this._message.complete(); - } else { - this._error.next(ev); - } - } - }; - - this.socket.onerror = (ev: ErrorEvent) => { - this.errorThrown = true; - this._error.next(ev); - }; - } catch (error) { - this.loggerService.debug('Web EventsSocket connection error', error); - this._error.next(error); - } - } - - close(): void { - if (this.socket && this.socket.readyState !== 3) { - this.socket.close(); - } - } -} diff --git a/core-web/libs/dotcms-js/src/public_api.ts b/core-web/libs/dotcms-js/src/public_api.ts index 9cc178ff30b1..fd650e303f60 100644 --- a/core-web/libs/dotcms-js/src/public_api.ts +++ b/core-web/libs/dotcms-js/src/public_api.ts @@ -4,26 +4,21 @@ export * from './lib/core/browser-util.service'; export * from './lib/core/dot-push-publish-dialog.service'; export * from './lib/core/dot-router.service'; export * from './lib/core/dotcms-config.service'; -export * from './lib/core/dotcms-events.service'; export * from './lib/core/logger.service'; export * from './lib/core/login.service'; export * from './lib/core/routing.service'; export * from './lib/core/site.service'; export * from './lib/core/string-utils.service'; export * from './lib/core/util/app.config'; -export * from './lib/core/util/dot-event-socket'; export * from './lib/core/util/http-code'; export * from './lib/core/util/http-request-utils'; export * from './lib/core/util/http-response-util'; export * from './lib/core/util/local-store.service'; -export * from './lib/core/util/long-polling-protocol'; export * from './lib/core/util/notification.service'; export * from './lib/core/util/protocol'; export * from './lib/core/util/response-view'; -export * from './lib/core/util/websockets-protocol'; // MODELS export * from './lib/core/models'; export * from './lib/core/shared/user.model'; export * from './lib/core/site.service.mock'; -export * from './lib/core/util/models/dot-event-socket-url'; diff --git a/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-list/dot-experiments-list.component.spec.ts b/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-list/dot-experiments-list.component.spec.ts index 9936d6e85c19..4c52897e1d92 100644 --- a/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-list/dot-experiments-list.component.spec.ts +++ b/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-list/dot-experiments-list.component.spec.ts @@ -17,7 +17,6 @@ import { } from '@dotcms/data-access'; import { DotPushPublishDialogService, - DotcmsEventsService, LoginService, DotcmsConfigService, LoggerService @@ -68,7 +67,6 @@ describe('DotExperimentsListComponent', () => { DotHttpErrorManagerService, mockProvider(DotExperimentsService), mockProvider(DotPushPublishDialogService), - mockProvider(DotcmsEventsService), mockProvider(LoginService), mockProvider(LoggerService), mockProvider(DotFormatDateService), diff --git a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts index 3ae30a29e201..7f7c50b552a7 100644 --- a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts +++ b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts @@ -13,7 +13,8 @@ import { DotMessageService, DotOsgiService } from '@dotcms/data-access'; -import { DotcmsEventsService, DotPushPublishDialogService } from '@dotcms/dotcms-js'; +import { DotPushPublishDialogService } from '@dotcms/dotcms-js'; +import { DotEventsSocket } from '@dotcms/data-access'; import { DotEnvironment } from '@dotcms/dotcms-models'; import { DotPluginsListComponent } from './dot-plugins-list.component'; @@ -46,7 +47,7 @@ describe('DotPluginsListComponent', () => { mockProvider(DotHttpErrorManagerService), ConfirmationService, mockProvider(DotMessageDisplayService, { push: jest.fn() }), - mockProvider(DotcmsEventsService, { subscribeTo: jest.fn().mockReturnValue(EMPTY) }), + mockProvider(DotEventsSocket, { on: jest.fn().mockReturnValue(EMPTY) }), mockProvider(ActivatedRoute, { snapshot: { data: { pushPublishEnvironments: [], isEnterprise: false } } }), diff --git a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.spec.ts b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.spec.ts index 93d7c355fb12..074e5645a8d0 100644 --- a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.spec.ts +++ b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.spec.ts @@ -5,12 +5,12 @@ import { fakeAsync, tick } from '@angular/core/testing'; import { BundleMap, + DotEventsSocket, DotHttpErrorManagerService, DotMessageDisplayService, DotMessageService, DotOsgiService } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotCMSAPIResponse, DotMessageSeverity } from '@dotcms/dotcms-models'; import { DotPluginsListStore } from './dot-plugins-list.store'; @@ -96,8 +96,8 @@ describe('DotPluginsListStore', () => { mockProvider(DotHttpErrorManagerService, { handle: jest.fn() }), mockProvider(DotMessageDisplayService, { push: jest.fn() }), mockProvider(DotMessageService, { get: (key: string) => key }), - mockProvider(DotcmsEventsService, { - subscribeTo: jest.fn().mockImplementation((event: string) => { + mockProvider(DotEventsSocket, { + on: jest.fn().mockImplementation((event: string) => { if (event === 'OSGI_FRAMEWORK_RESTART') return osgiFrameworkRestartSubject.asObservable(); if (event === 'OSGI_BUNDLES_LOADED') diff --git a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.ts b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.ts index 7b189589c3af..3dd4a447873d 100644 --- a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.ts +++ b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/store/dot-plugins-list.store.ts @@ -15,13 +15,13 @@ import { catchError, debounceTime, delay, take } from 'rxjs/operators'; import { BundleMap, + DotEventsSocket, DotHttpErrorManagerService, DotMessageDisplayService, DotMessageService, DotOsgiService, PluginRow } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotEnvironment, DotMessageSeverity, DotMessageType } from '@dotcms/dotcms-models'; /** Delay after OSGi mutating calls before reload; matches backend / websocket timing for bundle state to settle. */ @@ -268,20 +268,20 @@ export const DotPluginsListStore = signalStore( withHooks((store) => ({ /** Initial full load; listens for OSGi websocket events to update status and refresh data. */ onInit() { - const dotcmsEventsService = inject(DotcmsEventsService); + const eventsSocket = inject(DotEventsSocket); const dotMessageDisplayService = inject(DotMessageDisplayService); const dotMessageService = inject(DotMessageService); const destroyRef = inject(DestroyRef); store.loadAll(undefined, true); - dotcmsEventsService - .subscribeTo('OSGI_FRAMEWORK_RESTART') + eventsSocket + .on('OSGI_FRAMEWORK_RESTART') .pipe(takeUntilDestroyed(destroyRef)) .subscribe(() => patchState(store, { status: 'restarting' })); - dotcmsEventsService - .subscribeTo('OSGI_BUNDLES_LOADED') + eventsSocket + .on('OSGI_BUNDLES_LOADED') .pipe(debounceTime(OSGI_ACTION_DELAY_MS), takeUntilDestroyed(destroyRef)) .subscribe(() => store.loadAll( @@ -291,8 +291,8 @@ export const DotPluginsListStore = signalStore( ) ); - dotcmsEventsService - .subscribeTo('OSGI_BUNDLES_UPLOAD_FAILED') + eventsSocket + .on('OSGI_BUNDLES_UPLOAD_FAILED') .pipe(takeUntilDestroyed(destroyRef)) .subscribe(() => { patchState(store, { status: 'loaded' }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.spec.ts index b3253467116a..d2a1d2b79ff9 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.spec.ts @@ -27,15 +27,11 @@ import { DotWorkflowActionsFireService, PushPublishService } from '@dotcms/data-access'; -import { DotcmsConfigService, DotcmsEventsService } from '@dotcms/dotcms-js'; +import { DotcmsConfigService } from '@dotcms/dotcms-js'; import { DotCMSBaseTypesContentTypes } from '@dotcms/dotcms-models'; import { DotContentCompareComponent } from '@dotcms/portlets/dot-ema/ui'; import { DotCMSPage, DotCMSURLContentMap, DotCMSUVEAction } from '@dotcms/types'; -import { - DotcmsConfigServiceMock, - DotcmsEventsServiceMock, - MockDotMessageService -} from '@dotcms/utils-testing'; +import { DotcmsConfigServiceMock, MockDotMessageService } from '@dotcms/utils-testing'; import { DotEmaDialogComponent } from './dot-ema-dialog.component'; import { DotEmaDialogStore } from './store/dot-ema-dialog.store'; @@ -99,10 +95,6 @@ describe('DotEmaDialogComponent', () => { provide: DotcmsConfigService, useValue: new DotcmsConfigServiceMock() }, - { - provide: DotcmsEventsService, - useValue: new DotcmsEventsServiceMock() - }, { provide: PushPublishService, useValue: { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts index d59e21335969..2e0bee22a565 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts @@ -36,13 +36,7 @@ import { DotWorkflowsActionsService, PushPublishService } from '@dotcms/data-access'; -import { - DotcmsConfigService, - DotcmsEventsService, - LoginService, - Site, - SiteService -} from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoginService, Site, SiteService } from '@dotcms/dotcms-js'; import { FeaturedFlags } from '@dotcms/dotcms-models'; import { DotPageScannerReportComponent, @@ -57,7 +51,6 @@ import { DotCurrentUserServiceMock, DotLanguagesServiceMock, DotcmsConfigServiceMock, - DotcmsEventsServiceMock, SiteServiceMock } from '@dotcms/utils-testing'; @@ -274,10 +267,6 @@ describe('DotEmaShellComponent', () => { provide: DotcmsConfigService, useValue: new DotcmsConfigServiceMock() }, - { - provide: DotcmsEventsService, - useValue: new DotcmsEventsServiceMock() - }, { provide: PushPublishService, useValue: { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts index fc45ea633722..18aed44ae449 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts @@ -53,7 +53,7 @@ import { DotWorkflowsActionsService, PushPublishService } from '@dotcms/data-access'; -import { DotcmsConfigService, DotcmsEventsService, LoginService } from '@dotcms/dotcms-js'; +import { DotcmsConfigService, LoginService } from '@dotcms/dotcms-js'; import { DEFAULT_VARIANT_ID, FeaturedFlags } from '@dotcms/dotcms-models'; import { DotResultsSeoToolComponent } from '@dotcms/portlets/dot-ema/ui'; import { GlobalStore } from '@dotcms/store'; @@ -68,7 +68,6 @@ import { DotLanguagesServiceMock, DotPersonalizeServiceMock, DotcmsConfigServiceMock, - DotcmsEventsServiceMock, LoginServiceMock, MockDotHttpErrorManagerService, MockDotMessageService, @@ -349,10 +348,6 @@ const createRouting = () => provide: DotcmsConfigService, useValue: new DotcmsConfigServiceMock() }, - { - provide: DotcmsEventsService, - useValue: new DotcmsEventsServiceMock() - }, { provide: PushPublishService, useValue: { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-ema-workflow-actions/dot-ema-workflow-actions.service.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-ema-workflow-actions/dot-ema-workflow-actions.service.spec.ts index c4699fd084ef..d2dbf1df7f41 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-ema-workflow-actions/dot-ema-workflow-actions.service.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-ema-workflow-actions/dot-ema-workflow-actions.service.spec.ts @@ -12,7 +12,7 @@ import { DotMessageService, DotFormatDateService } from '@dotcms/data-access'; -import { DotEventsSocketURL, LoginService } from '@dotcms/dotcms-js'; +import { LoginService } from '@dotcms/dotcms-js'; import { DotCMSWorkflowActionEvent, DotWizardStep, @@ -33,13 +33,6 @@ import { import { DotEmaWorkflowActionsService } from './dot-ema-workflow-actions.service'; -const dotEventSocketURLFactory = () => { - return new DotEventsSocketURL( - `${window.location.hostname}:${window.location.port}/api/ws/v1/system/events`, - window.location.protocol === 'https:' - ); -}; - @Injectable() export class MockPushPublishService { getEnvironments() { @@ -143,7 +136,6 @@ describe('DotEmaWorkflowActionsService', () => { provide: LoginService, useClass: LoginServiceMock }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: DotFormatDateService, useClass: DotFormatDateServiceMock } ] }); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-actions/template-builder-actions.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-actions/template-builder-actions.component.spec.ts index ea9abf2e4f56..b8173de7590c 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-actions/template-builder-actions.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-actions/template-builder-actions.component.spec.ts @@ -8,10 +8,9 @@ import { DotMessageService, DotSystemConfigService } from '@dotcms/data-access'; -import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { GlobalStore } from '@dotcms/store'; import { DotMessagePipe } from '@dotcms/ui'; -import { DotCurrentUserServiceMock, DotcmsEventsServiceMock } from '@dotcms/utils-testing'; +import { DotCurrentUserServiceMock } from '@dotcms/utils-testing'; import { TemplateBuilderActionsComponent } from './template-builder-actions.component'; @@ -42,10 +41,6 @@ describe('TemplateBuilderActionsComponent', () => { provide: GlobalStore, useValue: mockGlobalStore }, - { - provide: DotcmsEventsService, - useClass: DotcmsEventsServiceMock - }, DotTemplateBuilderStore ], imports: [HttpClientTestingModule, DotMessagePipe] diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts index 78572a21cc82..7e854c23e914 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts @@ -17,11 +17,10 @@ import { DotMessageService, DotSystemConfigService } from '@dotcms/data-access'; -import { DotcmsEventsService, LoginService, SiteService } from '@dotcms/dotcms-js'; +import { LoginService, SiteService } from '@dotcms/dotcms-js'; import { GlobalStore } from '@dotcms/store'; import { containersMock, - DotcmsEventsServiceMock, DotContainersServiceMock, DotCurrentUserServiceMock, LoginServiceMock, @@ -101,10 +100,6 @@ describe('TemplateBuilderComponent', () => { provide: GlobalStore, useValue: { currentSiteId: () => null } }, - { - provide: DotcmsEventsService, - useClass: DotcmsEventsServiceMock - }, DotEventsService ] }); diff --git a/core-web/libs/utils-testing/src/index.ts b/core-web/libs/utils-testing/src/index.ts index c0cca0108bbf..177c443469ec 100644 --- a/core-web/libs/utils-testing/src/index.ts +++ b/core-web/libs/utils-testing/src/index.ts @@ -36,7 +36,6 @@ export * from './lib/dot-workflow-service.mock'; export * from './lib/dot-workflows-actions.mock'; export * from './lib/dotcms-config.service.mock'; export * from './lib/dotcms-contentlet.mock'; -export * from './lib/dotcms-events-service.mock'; export * from './lib/fake-event.mock'; export * from './lib/field-variable-service.mock'; export * from './lib/format-date-service.mock'; diff --git a/core-web/libs/utils-testing/src/lib/dotcms-events-service.mock.ts b/core-web/libs/utils-testing/src/lib/dotcms-events-service.mock.ts deleted file mode 100644 index 825b818d4236..000000000000 --- a/core-web/libs/utils-testing/src/lib/dotcms-events-service.mock.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Observable, Subject } from 'rxjs'; - -import { DotEventTypeWrapper } from '@dotcms/dotcms-js'; - -export class DotcmsEventsServiceMock { - private observers: Subject[] = []; - - subscribeTo(clientEventType: string): Observable> { - if (!this.observers[clientEventType]) { - this.observers[clientEventType] = new Subject(); - } - - return this.observers[clientEventType].asObservable(); - } - - subscribeToEvents(clientEventTypes: string[]): Observable> { - const subject: Subject> = new Subject< - DotEventTypeWrapper - >(); - - clientEventTypes.forEach((eventType) => - this.subscribeTo(eventType).subscribe((data) => subject.next(data)) - ); - - return subject.asObservable(); - } - - triggerSubscribeTo(clientEventType: string, data: unknown): void { - this.observers[clientEventType].next(data); - } - - triggerSubscribeToEvents(clientEventTypes: string[], data: unknown): void { - clientEventTypes.forEach((eventType) => { - this.observers[eventType].next({ - eventType: eventType, - data: data - }); - }); - } -} From 3ed834abc8f62e4fcdda5f5ee27f47ebce7e5aa4 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 11:31:18 -0600 Subject: [PATCH 29/36] fix(dotcms-ui): fix import order and unused EMPTY imports in spec files Co-Authored-By: Claude Sonnet 4.6 --- .../dot-custom-event-handler.service.spec.ts | 2 +- .../dot-contentlets/dot-contentlets.component.spec.ts | 2 +- .../dot-porlet-detail/dot-portlet-detail.component.spec.ts | 2 +- .../dot-workflow-task/dot-workflow-task.component.spec.ts | 2 +- .../components/form/content-types-form.component.spec.ts | 2 +- .../dot-content-type-copy-dialog.component.spec.ts | 2 +- .../iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts | 2 +- .../dot-add-persona-dialog.component.spec.ts | 2 +- .../dot-add-contentlet/dot-add-contentlet.component.spec.ts | 2 +- .../dot-contentlet-wrapper.component.spec.ts | 2 +- .../dot-reorder-menu/dot-reorder-menu.component.spec.ts | 2 +- .../dot-persona-selector/dot-persona-selector.component.spec.ts | 2 +- .../view/components/main-legacy/main-legacy.component.spec.ts | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts index c8a2180da10e..aedfe5b78582 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { of } from 'rxjs'; @@ -50,6 +49,7 @@ import { import { DotCustomEventHandlerService } from './dot-custom-event-handler.service'; +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; import { DotContentletEditorService } from '../../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; import { DotDownloadBundleDialogService } from '../dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from '../dot-menu.service'; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts index 3dcac85ea7b1..6e5328fd736d 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; @@ -50,6 +49,7 @@ import { DotContentletsComponent } from './dot-contentlets.component'; import { DotCustomEventHandlerService } from '../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotDownloadBundleDialogService } from '../../../api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../view/components/_common/iframe/service/iframe-overlay.service'; import { DotEditContentletComponent } from '../../../view/components/dot-contentlet-editor/components/dot-edit-contentlet/dot-edit-contentlet.component'; import { DotContentletEditorService } from '../../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts index 076c147a9c32..035b1f14e729 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts @@ -43,11 +43,11 @@ import { LoginServiceMock, MockDotRouterService } from '@dotcms/utils-testing'; import { DotPortletDetailComponent } from './dot-portlet-detail.component'; -import { MockDotUiColorsService } from '../../test/dot-test-bed'; import { DotCustomEventHandlerService } from '../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotDownloadBundleDialogService } from '../../api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from '../../api/services/dot-menu.service'; +import { MockDotUiColorsService } from '../../test/dot-test-bed'; import { DotDownloadBundleDialogComponent } from '../../view/components/_common/dot-download-bundle-dialog/dot-download-bundle-dialog.component'; import { IframeOverlayService } from '../../view/components/_common/iframe/service/iframe-overlay.service'; import { DotContentletEditorService } from '../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts index fdd6c50ee65d..80b200526f2d 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { mockProvider } from '@ngneat/spectator/jest'; @@ -56,6 +55,7 @@ import { DotWorkflowTaskComponent } from './dot-workflow-task.component'; import { DotCustomEventHandlerService } from '../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotDownloadBundleDialogService } from '../../../api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from '../../../api/services/dot-menu.service'; +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../view/components/_common/iframe/service/iframe-overlay.service'; import { DotContentletEditorService } from '../../../view/components/dot-contentlet-editor/services/dot-contentlet-editor.service'; import { DotWorkflowTaskDetailComponent } from '../../../view/components/dot-workflow-task-detail/dot-workflow-task-detail.component'; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts index 18ec877400f1..c6e2da60eeff 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; -import { EMPTY, Observable, of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts index 061daae08562..bbbe3bb3f587 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/components/dot-content-type-copy-dialog/dot-content-type-copy-dialog.component.spec.ts @@ -1,4 +1,4 @@ -import { EMPTY, of } from 'rxjs'; +import { of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts index a6b94522f06f..6def81845cbd 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-porlet-legacy/iframe-porlet-legacy.component.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { EMPTY, of } from 'rxjs'; @@ -48,6 +47,7 @@ import { IframePortletLegacyComponent } from './iframe-porlet-legacy.component'; import { DotCustomEventHandlerService } from '../../../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotMenuService } from '../../../../../api/services/dot-menu.service'; +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { DotContentletEditorService } from '../../../dot-contentlet-editor/services/dot-contentlet-editor.service'; import { DotDownloadBundleDialogComponent } from '../../dot-download-bundle-dialog/dot-download-bundle-dialog.component'; import { IFrameModule } from '../index'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts index 0fef810ce93e..f1f2f1e68bab 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-add-persona-dialog.component.spec.ts @@ -1,5 +1,5 @@ import { createComponentFactory, Spectator, byTestId, mockProvider } from '@ngneat/spectator/jest'; -import { EMPTY, of, throwError } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts index e097b0fc2b6b..71fef74d932c 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-add-contentlet/dot-add-contentlet.component.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; @@ -40,6 +39,7 @@ import { DotAddContentletComponent } from './dot-add-contentlet.component'; import { DotCustomEventHandlerService } from '../../../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotMenuService } from '../../../../../api/services/dot-menu.service'; +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../_common/iframe/service/iframe-overlay.service'; import { DotIframeDialogComponent } from '../../../dot-iframe-dialog/dot-iframe-dialog.component'; import { DotContentletEditorService } from '../../services/dot-contentlet-editor.service'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts index a9d443fff2d6..15feb53f1c37 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { of } from 'rxjs'; @@ -33,6 +32,7 @@ import { DotContentletWrapperComponent } from './dot-contentlet-wrapper.componen import { DotCustomEventHandlerService } from '../../../../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotMenuService } from '../../../../../api/services/dot-menu.service'; +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../_common/iframe/service/iframe-overlay.service'; import { DotIframeDialogComponent } from '../../../dot-iframe-dialog/dot-iframe-dialog.component'; import { DotContentletEditorService } from '../../services/dot-contentlet-editor.service'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts index e449def80f8b..4c5086237fb8 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-reorder-menu/dot-reorder-menu.component.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { of, Subject } from 'rxjs'; import { DebugElement } from '@angular/core'; @@ -25,6 +24,7 @@ import { import { DotReorderMenuComponent } from './dot-reorder-menu.component'; +import { MockDotUiColorsService } from '../../../../../test/dot-test-bed'; import { IframeOverlayService } from '../../../_common/iframe/service/iframe-overlay.service'; import { DotIframeDialogComponent } from '../../../dot-iframe-dialog/dot-iframe-dialog.component'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts index 9d2aa25cdc4c..2fbd77cdf2eb 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts @@ -1,5 +1,5 @@ import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; -import { EMPTY, of } from 'rxjs'; +import { of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts index efeae12dfbf2..36286385afc1 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts @@ -1,4 +1,3 @@ -import { MockDotUiColorsService } from '../../../test/dot-test-bed'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { mockProvider } from '@ngneat/spectator/jest'; @@ -52,6 +51,7 @@ import { DotDownloadBundleDialogService } from '../../../api/services/dot-downlo import { DotMenuService } from '../../../api/services/dot-menu.service'; import { NotificationsService } from '../../../api/services/notifications-service'; import { LOCATION_TOKEN } from '../../../providers'; +import { MockDotUiColorsService } from '../../../test/dot-test-bed'; import { DotDownloadBundleDialogComponent } from '../_common/dot-download-bundle-dialog/dot-download-bundle-dialog.component'; import { DotWizardComponent } from '../_common/dot-wizard/dot-wizard.component'; import { IframeOverlayService } from '../_common/iframe/service/iframe-overlay.service'; From 590946fe024b6c33bdbf23a6bec0e5a2d9d1617c Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 11:55:37 -0600 Subject: [PATCH 30/36] fix(dotcms-js, dot-plugins): fix import order and deduplicate DotEventsSocket import Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts index 7f7c50b552a7..e7ebc1b74a2c 100644 --- a/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts +++ b/core-web/libs/portlets/dot-plugins/src/lib/dot-plugins-list/dot-plugins-list.component.spec.ts @@ -8,13 +8,13 @@ import { DialogService } from 'primeng/dynamicdialog'; import { BUNDLE_STATE, + DotEventsSocket, DotHttpErrorManagerService, DotMessageDisplayService, DotMessageService, DotOsgiService } from '@dotcms/data-access'; import { DotPushPublishDialogService } from '@dotcms/dotcms-js'; -import { DotEventsSocket } from '@dotcms/data-access'; import { DotEnvironment } from '@dotcms/dotcms-models'; import { DotPluginsListComponent } from './dot-plugins-list.component'; From 55b0ef126077ed2e1712414691924da30e5371d6 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 12:19:06 -0600 Subject: [PATCH 31/36] fix(dotcms-js): suppress enforce-module-boundaries for DotEventsSocket import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dotcms-js → data-access is a known architectural constraint; the circular dep lint rule fires because data-access re-exports symbols that import back into dotcms-js. Suppress on these two lines only. Co-Authored-By: Claude Sonnet 4.6 --- core-web/libs/dotcms-js/src/lib/core/login.service.ts | 1 + core-web/libs/dotcms-js/src/lib/core/routing.service.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/core-web/libs/dotcms-js/src/lib/core/login.service.ts b/core-web/libs/dotcms-js/src/lib/core/login.service.ts index 978401269bee..f12e6f0eb3da 100644 --- a/core-web/libs/dotcms-js/src/lib/core/login.service.ts +++ b/core-web/libs/dotcms-js/src/lib/core/login.service.ts @@ -7,6 +7,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { map, tap } from 'rxjs/operators'; +// eslint-disable-next-line @nx/enforce-module-boundaries import { DotEventsSocket } from '@dotcms/data-access'; import { DotCMSResponse, diff --git a/core-web/libs/dotcms-js/src/lib/core/routing.service.ts b/core-web/libs/dotcms-js/src/lib/core/routing.service.ts index 24d94604a64b..acda000103b2 100644 --- a/core-web/libs/dotcms-js/src/lib/core/routing.service.ts +++ b/core-web/libs/dotcms-js/src/lib/core/routing.service.ts @@ -6,6 +6,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { map } from 'rxjs/operators'; +// eslint-disable-next-line @nx/enforce-module-boundaries import { DotEventsSocket } from '@dotcms/data-access'; import { DotCMSResponse } from '@dotcms/dotcms-models'; From bd96276febd986336d5a89c35878df3494c68641 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 13:25:22 -0600 Subject: [PATCH 32/36] ci: trigger CI rerun From 46199876fe74334e5d09b4256b67c0c2c8f60429 Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 15:16:32 -0600 Subject: [PATCH 33/36] fix lint --- .../src/lib/core/dotcms-events.service.ts | 53 ++++++++++ .../dotcms-js/src/lib/core/login.service.ts | 29 +++--- .../dotcms-js/src/lib/core/routing.service.ts | 13 +-- .../with-websocket/with-websocket.feature.ts | 98 ++++++++++++------- 4 files changed, 129 insertions(+), 64 deletions(-) create mode 100644 core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts diff --git a/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts b/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts new file mode 100644 index 000000000000..21ad07ae8efe --- /dev/null +++ b/core-web/libs/dotcms-js/src/lib/core/dotcms-events.service.ts @@ -0,0 +1,53 @@ +import { Observable, Subject } from 'rxjs'; + +import { Injectable } from '@angular/core'; + +import { DotEventTypeWrapper } from './models'; + +/** + * @deprecated Use DotEventsSocket from @dotcms/data-access directly. + * + * This is a pure Subject-based event bus with no WebSocket logic. + * Messages are fed into it by withWebSocket() (global-store) via feedMessage(), + * keeping dotcms-js free of any data-access imports and avoiding circular deps. + * + * start() and destroy() are intentional no-ops — DotEventsSocket owns the connection. + */ +@Injectable({ providedIn: 'root' }) +export class DotcmsEventsService { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private subjects: Record> = {}; + + /** Called by withWebSocket() on each incoming message to fan out to subscribers. */ + feedMessage(event: string, data: unknown): void { + if (this.subjects[event]) { + this.subjects[event].next(data); + } + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + start(): void {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + destroy(): void {} + + subscribeTo(clientEventType: string): Observable { + if (!this.subjects[clientEventType]) { + this.subjects[clientEventType] = new Subject(); + } + + return this.subjects[clientEventType].asObservable(); + } + + subscribeToEvents(clientEventTypes: string[]): Observable> { + const subject = new Subject>(); + + clientEventTypes.forEach((eventType) => { + this.subscribeTo(eventType).subscribe((data) => { + subject.next({ data, name: eventType }); + }); + }); + + return subject.asObservable(); + } +} diff --git a/core-web/libs/dotcms-js/src/lib/core/login.service.ts b/core-web/libs/dotcms-js/src/lib/core/login.service.ts index f12e6f0eb3da..ef1186eafb84 100644 --- a/core-web/libs/dotcms-js/src/lib/core/login.service.ts +++ b/core-web/libs/dotcms-js/src/lib/core/login.service.ts @@ -3,18 +3,17 @@ import { Observable, of, Subject } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { map, tap } from 'rxjs/operators'; -// eslint-disable-next-line @nx/enforce-module-boundaries -import { DotEventsSocket } from '@dotcms/data-access'; import { DotCMSResponse, DotLoginInformation, SESSION_STORAGE_VARIATION_KEY } from '@dotcms/dotcms-models'; +import { DotcmsEventsService } from './dotcms-events.service'; + export interface DotLoginParams { login: string; password: string; @@ -34,7 +33,7 @@ export const LOGOUT_URL = '/dotAdmin/logout'; }) export class LoginService { private http = inject(HttpClient); - private eventsSocket = inject(DotEventsSocket); + private dotcmsEventsService = inject(DotcmsEventsService); currentUserLanguageId = ''; private country = ''; @@ -56,20 +55,14 @@ export class LoginService { current: '/api/v1/users/current/' }; - this.eventsSocket - .on('SESSION_DESTROYED') - .pipe(takeUntilDestroyed()) - .subscribe(() => { - this.logOutUser(); - this.clearExperimentPersistence(); - }); - - this.eventsSocket - .on('SESSION_LOGOUT') - .pipe(takeUntilDestroyed()) - .subscribe(() => { - this.clearExperimentPersistence(); - }); + this.dotcmsEventsService.subscribeTo('SESSION_DESTROYED').subscribe(() => { + this.logOutUser(); + this.clearExperimentPersistence(); + }); + + this.dotcmsEventsService.subscribeTo('SESSION_LOGOUT').subscribe(() => { + this.clearExperimentPersistence(); + }); } private _auth$: Subject = new Subject(); diff --git a/core-web/libs/dotcms-js/src/lib/core/routing.service.ts b/core-web/libs/dotcms-js/src/lib/core/routing.service.ts index acda000103b2..037fb990323c 100644 --- a/core-web/libs/dotcms-js/src/lib/core/routing.service.ts +++ b/core-web/libs/dotcms-js/src/lib/core/routing.service.ts @@ -2,15 +2,13 @@ import { Observable, Subject } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { map } from 'rxjs/operators'; -// eslint-disable-next-line @nx/enforce-module-boundaries -import { DotEventsSocket } from '@dotcms/data-access'; import { DotCMSResponse } from '@dotcms/dotcms-models'; import { DotRouterService } from './dot-router.service'; +import { DotcmsEventsService } from './dotcms-events.service'; import { LoginService } from './login.service'; @Injectable() @@ -30,16 +28,15 @@ export class RoutingService { // TODO: I think we should be able to remove the routing injection constructor() { const loginService = inject(LoginService); - const eventsSocket = inject(DotEventsSocket); + const dotcmsEventsService = inject(DotcmsEventsService); this.urlMenus = '/api/v1/CORE_WEB/menu'; this.portlets = new Map(); loginService.watchUser(this.loadMenus.bind(this)); - eventsSocket - .on('UPDATE_PORTLET_LAYOUTS') - .pipe(takeUntilDestroyed()) - .subscribe(() => this.loadMenus()); + dotcmsEventsService + .subscribeTo('UPDATE_PORTLET_LAYOUTS') + .subscribe(this.loadMenus.bind(this)); } get currentPortletId(): string { diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts index 341e835ed8e2..35f60c06243c 100644 --- a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts @@ -7,6 +7,7 @@ import { inject } from '@angular/core'; import { switchMap, tap } from 'rxjs/operators'; import { DotEventsSocket, WebSocketStatus } from '@dotcms/data-access'; +import { DotcmsEventsService } from '@dotcms/dotcms-js'; import { DotSite } from '@dotcms/dotcms-models'; export interface WebSocketState { @@ -30,51 +31,72 @@ const initialWebSocketState: WebSocketState = { export function withWebSocket() { return signalStoreFeature( withState(initialWebSocketState), - withMethods((store, eventsSocket = inject(DotEventsSocket)) => ({ - startConnection: rxMethod(pipe(switchMap(() => eventsSocket.connect()))), - trackStatus: rxMethod( - pipe( - switchMap(() => - eventsSocket - .status$() - .pipe(tap((wsStatus) => patchState(store, { wsStatus }))) + withMethods( + ( + store, + eventsSocket = inject(DotEventsSocket), + dotcmsEventsService = inject(DotcmsEventsService) + ) => ({ + startConnection: rxMethod(pipe(switchMap(() => eventsSocket.connect()))), + trackStatus: rxMethod( + pipe( + switchMap(() => + eventsSocket + .status$() + .pipe(tap((wsStatus) => patchState(store, { wsStatus }))) + ) + ) + ), + /** Pipes all raw WS messages into the legacy DotcmsEventsService Subject bus. */ + feedLegacyEventBus: rxMethod( + pipe( + switchMap(() => + eventsSocket + .messages() + .pipe( + tap(({ event, payload }) => + dotcmsEventsService.feedMessage(event, payload?.data) + ) + ) + ) ) - ) - ), - destroySocket: () => eventsSocket.destroy(), - /** - * Observable that emits when the backend sends UPDATE_PORTLET_LAYOUTS. - * Use this instead of the deprecated DotcmsEventsService. - */ - portletLayoutUpdated$: (): Observable => - eventsSocket.on('UPDATE_PORTLET_LAYOUTS'), - - /** - * Observable that emits whenever a site is created, published, - * archived, unarchived, or updated. Use this to refresh site lists. - */ - siteEvents$: (): Observable => - merge( - eventsSocket.on('SAVE_SITE'), - eventsSocket.on('PUBLISH_SITE'), - eventsSocket.on('UN_PUBLISH_SITE'), - eventsSocket.on('UPDATE_SITE'), - eventsSocket.on('ARCHIVE_SITE'), - eventsSocket.on('UN_ARCHIVE_SITE'), - eventsSocket.on('DELETE_SITE') ), + destroySocket: () => eventsSocket.destroy(), + /** + * Observable that emits when the backend sends UPDATE_PORTLET_LAYOUTS. + * Use this instead of the deprecated DotcmsEventsService. + */ + portletLayoutUpdated$: (): Observable => + eventsSocket.on('UPDATE_PORTLET_LAYOUTS'), + + /** + * Observable that emits whenever a site is created, published, + * archived, unarchived, or updated. Use this to refresh site lists. + */ + siteEvents$: (): Observable => + merge( + eventsSocket.on('SAVE_SITE'), + eventsSocket.on('PUBLISH_SITE'), + eventsSocket.on('UN_PUBLISH_SITE'), + eventsSocket.on('UPDATE_SITE'), + eventsSocket.on('ARCHIVE_SITE'), + eventsSocket.on('UN_ARCHIVE_SITE'), + eventsSocket.on('DELETE_SITE') + ), - /** - * Observable that emits the new site when another user/tab switches - * the current site (SWITCH_SITE event). The payload contains the full - * DotSite object — no extra HTTP call needed. - */ - switchSiteEvent$: (): Observable => eventsSocket.on('SWITCH_SITE') - })), + /** + * Observable that emits the new site when another user/tab switches + * the current site (SWITCH_SITE event). The payload contains the full + * DotSite object — no extra HTTP call needed. + */ + switchSiteEvent$: (): Observable => eventsSocket.on('SWITCH_SITE') + }) + ), withHooks({ onInit(store) { store.startConnection(); store.trackStatus(); + store.feedLegacyEventBus(); }, onDestroy(store) { store.destroySocket(); From 38c4d50d37f8581cb5f3ca4e72be5a419d40073a Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 15:32:36 -0600 Subject: [PATCH 34/36] fix format --- .../dot-porlet-detail/dot-portlet-detail.component.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts index 035b1f14e729..6047abf273be 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts @@ -43,7 +43,6 @@ import { LoginServiceMock, MockDotRouterService } from '@dotcms/utils-testing'; import { DotPortletDetailComponent } from './dot-portlet-detail.component'; - import { DotCustomEventHandlerService } from '../../api/services/dot-custom-event-handler/dot-custom-event-handler.service'; import { DotDownloadBundleDialogService } from '../../api/services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotMenuService } from '../../api/services/dot-menu.service'; From dcca6e96df0c8283d726fad20b1ec87cbdc591ea Mon Sep 17 00:00:00 2001 From: Freddy Montes Date: Fri, 17 Apr 2026 17:05:10 -0600 Subject: [PATCH 35/36] fix(global-store): export DotcmsEventsService from dotcms-js barrel and fix TypeScript type in withWebSocket - Add DotcmsEventsService to public_api.ts so @dotcms/dotcms-js resolves it - Explicit type annotation on dotcmsEventsService inject() call to satisfy strict TS Co-Authored-By: Claude Sonnet 4.6 --- core-web/libs/dotcms-js/src/public_api.ts | 1 + .../src/lib/features/with-websocket/with-websocket.feature.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core-web/libs/dotcms-js/src/public_api.ts b/core-web/libs/dotcms-js/src/public_api.ts index fd650e303f60..37ca59de87b3 100644 --- a/core-web/libs/dotcms-js/src/public_api.ts +++ b/core-web/libs/dotcms-js/src/public_api.ts @@ -4,6 +4,7 @@ export * from './lib/core/browser-util.service'; export * from './lib/core/dot-push-publish-dialog.service'; export * from './lib/core/dot-router.service'; export * from './lib/core/dotcms-config.service'; +export * from './lib/core/dotcms-events.service'; export * from './lib/core/logger.service'; export * from './lib/core/login.service'; export * from './lib/core/routing.service'; diff --git a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts index 35f60c06243c..dc0896cfe86a 100644 --- a/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts +++ b/core-web/libs/global-store/src/lib/features/with-websocket/with-websocket.feature.ts @@ -35,7 +35,7 @@ export function withWebSocket() { ( store, eventsSocket = inject(DotEventsSocket), - dotcmsEventsService = inject(DotcmsEventsService) + dotcmsEventsService: DotcmsEventsService = inject(DotcmsEventsService) ) => ({ startConnection: rxMethod(pipe(switchMap(() => eventsSocket.connect()))), trackStatus: rxMethod( From f57a33cae52f4330c01809a16819080ac172e0d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 02:38:00 +0000 Subject: [PATCH 36/36] fix(websockets): address PR #35378 review feedback from Claude and Copilot - DotEventsSocket: guard OPEN state in openSocket() and cancel pending reconnect timer on manual connect() so repeat calls can never spawn a second concurrent WebSocket (Copilot, Claude #4) - dot-site.component: on switchSite(null) or getSiteById error, clear the CVA value via onSiteChange(null) instead of only nulling pinnedOption, so parent forms see the null value (Claude #1) - DotEventMessage.payload: make payload and payload.data optional to match the defensive handling already in on() (Claude #7) - GlobalStore.onInit: replace bare .subscribe() for switchSiteEvent$() with a lifecycle-tracked rxMethod so the subscription is torn down with the store (Claude #3, Copilot) - DotEventsSocket.messages(): document as @internal; consumers should use the typed on() API (Claude #8) https://claude.ai/code/session_01TYiVFjVWzBxdAnrJFZJv66 --- .../dot-websocket/dot-event-message.model.ts | 2 +- .../dot-events-socket.service.ts | 19 ++++++++++++++++-- core-web/libs/global-store/src/lib/store.ts | 20 ++++++++++++++----- .../components/dot-site/dot-site.component.ts | 6 ++++-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts index e048d80945fa..2d6eca1ff606 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-event-message.model.ts @@ -1,4 +1,4 @@ export interface DotEventMessage { event: string; - payload: { data: unknown }; + payload?: { data?: unknown }; } diff --git a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts index ad47b1b14069..fe7049d6cf82 100644 --- a/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts +++ b/core-web/libs/data-access/src/lib/dot-websocket/dot-events-socket.service.ts @@ -45,6 +45,8 @@ export class DotEventsSocket { connect(): Observable { return new Observable((subscriber) => { this.destroyed = false; + this.reconnectTimer?.unsubscribe(); + this.reconnectTimer = null; this.openSocket(); subscriber.next(); subscriber.complete(); @@ -69,7 +71,13 @@ export class DotEventsSocket { ); } - /** All raw messages from the server. */ + /** + * All raw messages from the server. + * + * @internal Consumers should use the typed `on(eventType)` API instead. + * This is kept public solely so `withWebSocket()` in @dotcms/global-store + * can pipe every message into the deprecated `DotcmsEventsService` bus. + */ messages(): Observable { return this._message.asObservable(); } @@ -84,7 +92,14 @@ export class DotEventsSocket { } private openSocket(): void { - if (this.destroyed || this.socket?.readyState === WebSocket.CONNECTING) { + if (this.destroyed) { + return; + } + + const state = this.socket?.readyState; + // Don't spawn a second socket if one is already live or still starting up. + // CLOSING (2) will fire onclose → scheduleReconnect, so no action needed here. + if (state === WebSocket.CONNECTING || state === WebSocket.OPEN) { return; } diff --git a/core-web/libs/global-store/src/lib/store.ts b/core-web/libs/global-store/src/lib/store.ts index 117f9d5fc1ba..24cf47855d78 100644 --- a/core-web/libs/global-store/src/lib/store.ts +++ b/core-web/libs/global-store/src/lib/store.ts @@ -155,6 +155,20 @@ export const GlobalStore = signalStore( ) ) ) + ), + + /** + * Keeps siteDetails in sync when another user/tab switches the site. + * Lifetime is tied to the store via rxMethod — no manual teardown needed. + */ + syncSiteOnSwitchEvent: rxMethod( + pipe( + switchMap(() => + store + .switchSiteEvent$() + .pipe(tap((site) => patchState(store, { siteDetails: site }))) + ) + ) ) }; }), @@ -164,11 +178,7 @@ export const GlobalStore = signalStore( withHooks({ onInit(store) { store.loadCurrentSite(); - // Keep siteDetails in sync when another user/tab switches the site - store - .switchSiteEvent$() - .pipe(tap((site) => patchState(store, { siteDetails: site }))) - .subscribe(); + store.syncSiteOnSwitchEvent(); } }) ); diff --git a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts index 14ea4a39fee1..09ea5b331a2e 100644 --- a/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-site/dot-site.component.ts @@ -706,14 +706,16 @@ export class DotSiteComponent implements ControlValueAccessor, OnInit, OnDestroy if (SITE_UNAVAILABLE_EVENTS.has(siteData.event ?? '')) { this.siteService.switchSite(null).subscribe({ next: (defaultSite) => this.onSiteChange(defaultSite), - error: () => patchState(this.$state, { pinnedOption: null }) + // Fall back to clearing the selection via the CVA path so the + // parent form sees the null value (not just a nulled pinnedOption). + error: () => this.onSiteChange(null) }); return; } this.siteService.getSiteById(pinned.identifier).subscribe({ next: (site) => patchState(this.$state, { pinnedOption: site }), - error: () => patchState(this.$state, { pinnedOption: null }) + error: () => this.onSiteChange(null) }); } }