Preview: Replaces WebSocket with the SignalR library to improve connectivity in the preview window (#20585)

* feat: replaces manual WebSocket with the actual SignalR library on the preview context

* feat: informs the developer what went wrong in preview mode

* feat: awaits the stop connection before proceeding

* feat: ensures no existing connection exists
This commit is contained in:
Jacob Overgaard
2025-10-21 15:20:17 +02:00
committed by GitHub
parent 58068d1aa7
commit de5a9ca5af
3 changed files with 65 additions and 32 deletions

View File

@@ -7,6 +7,9 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document';
import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { HubConnectionBuilder, type HubConnection } from '@umbraco-cms/backoffice/external/signalr';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions';
@@ -31,7 +34,7 @@ export class UmbPreviewContext extends UmbContextBase {
#culture?: string | null;
#segment?: string | null;
#serverUrl: string = '';
#webSocket?: WebSocket;
#connection?: HubConnection;
#iframeReady = new UmbBooleanState(false);
public readonly iframeReady = this.#iframeReady.asObservable();
@@ -41,12 +44,13 @@ export class UmbPreviewContext extends UmbContextBase {
#documentPreviewRepository = new UmbDocumentPreviewRepository(this);
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
#localize = new UmbLocalizationController(this);
constructor(host: UmbControllerHost) {
super(host, UMB_PREVIEW_CONTEXT);
this.consumeContext(UMB_SERVER_CONTEXT, (instance) => {
this.#serverUrl = instance?.getServerUrl() ?? '';
const params = new URLSearchParams(window.location.search);
this.#unique = params.get('id');
@@ -58,37 +62,62 @@ export class UmbPreviewContext extends UmbContextBase {
return;
}
this.#setPreviewUrl();
const serverUrl = instance?.getServerUrl();
if (!serverUrl) {
console.error('No server URL found in context');
return;
}
this.#serverUrl = serverUrl;
this.#setPreviewUrl({ serverUrl });
this.#initHubConnection(serverUrl);
});
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => {
this.#notificationContext = instance;
});
}
#configureWebSocket() {
if (this.#webSocket && this.#webSocket.readyState < 2) return;
async #initHubConnection(serverUrl: string) {
const previewHubUrl = `${serverUrl}/umbraco/PreviewHub`;
const url = `${this.#serverUrl.replace('https://', 'wss://')}/umbraco/PreviewHub`;
// Make sure that no previous connection exists.
if (this.#connection) {
await this.#connection.stop();
this.#connection = undefined;
}
this.#webSocket = new WebSocket(url);
this.#connection = new HubConnectionBuilder().withUrl(previewHubUrl).build();
this.#webSocket.addEventListener('open', () => {
// NOTE: SignalR protocol handshake; it requires a terminating control character.
const endChar = String.fromCharCode(30);
this.#webSocket?.send(`{"protocol":"json","version":1}${endChar}`);
});
this.#webSocket.addEventListener('message', (event: MessageEvent<string>) => {
if (!event?.data) return;
// NOTE: Strip the terminating control character, (from SignalR).
const data = event.data.substring(0, event.data.length - 1);
const json = JSON.parse(data) as { type: number; target: string; arguments: Array<string> };
if (json.type === 1 && json.target === 'refreshed') {
const pageId = json.arguments?.[0];
if (pageId === this.#unique) {
this.#setPreviewUrl({ rnd: Math.random() });
}
this.#connection.on('refreshed', (payload) => {
if (payload === this.#unique) {
this.#setPreviewUrl({ rnd: Math.random() });
}
});
this.#connection.onclose(() => {
this.#notificationContext?.peek('warning', {
data: {
headline: this.#localize.term('general_preview'),
message: this.#localize.term('preview_connectionLost'),
},
});
});
try {
await this.#connection.start();
} catch (error) {
console.error('The SignalR connection could not be established', error);
this.#notificationContext?.peek('warning', {
data: {
headline: this.#localize.term('general_preview'),
message: this.#localize.term('preview_connectionFailed'),
},
});
}
}
async #getPublishedUrl(): Promise<string | null> {
@@ -144,7 +173,7 @@ export class UmbPreviewContext extends UmbContextBase {
params.delete(segmentParam);
}
const previewUrl = new URL(url.pathname + '?' + params.toString(), host);
const previewUrl = new URL(`${url.pathname}?${params.toString()}`, host);
const previewUrlString = previewUrl.toString();
this.#previewUrl.setValue(previewUrlString);
@@ -180,9 +209,9 @@ export class UmbPreviewContext extends UmbContextBase {
await this.#documentPreviewRepository.exit();
}
if (this.#webSocket) {
this.#webSocket.close();
this.#webSocket = undefined;
if (this.#connection) {
await this.#connection.stop();
this.#connection = undefined;
}
let url = await this.#getPublishedUrl();
@@ -202,7 +231,6 @@ export class UmbPreviewContext extends UmbContextBase {
iframeLoaded(iframe: HTMLIFrameElement) {
if (!iframe) return;
this.#configureWebSocket();
this.#iframeReady.setValue(true);
}

View File

@@ -2603,13 +2603,16 @@ export default {
returnToPreviewHeadline: 'Forhåndsvisning af indholdet?',
returnToPreviewDescription:
'Du har afslutet forhåndsvisning, vil du starte forhåndsvisning igen for at\n se seneste gemte version af indholdet?\n ',
returnToPreviewAcceptButton: 'Start forhåndsvisning igen',
returnToPreviewDeclineButton: 'Se udgivet indhold',
viewPublishedContentHeadline: 'Se udgivet indhold?',
viewPublishedContentDescription:
'Du er i forhåndsvisning, vil du afslutte for at se den udgivet\n version?\n ',
viewPublishedContentAcceptButton: 'Se udgivet version',
viewPublishedContentDeclineButton: 'Forbliv i forhåndsvisning',
returnToPreviewAcceptButton: 'Preview latest version',
connectionFailed:
'Kunne ikke etablere forbindelse til serveren, forhåndsvisning af liveopdateringer vil ikke fungere.',
connectionLost: 'Forbindelse til serveren mistet, forhåndsvisning af liveopdateringer vil ikke fungere.',
},
permissions: {
FolderCreation: 'Mappeoprettelse',

View File

@@ -2749,6 +2749,8 @@ export default {
'You are in Preview Mode, do you want exit in order to view the published version of your website?',
viewPublishedContentAcceptButton: 'View published version',
viewPublishedContentDeclineButton: 'Stay in preview mode',
connectionFailed: 'Could not establish a connection to the server, preview live updates will not work.',
connectionLost: 'Connection to the server lost, preview live updates will not work.',
},
permissions: {
FolderCreation: 'Folder creation',