Restructure Communicator (#1510)

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-09-26 11:09:46 +02:00 committed by GitHub
parent e6830598d5
commit f1e91b4574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 680 additions and 569 deletions

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { WindowPostMessageCommunicator } from './window-post-message-communicator'
import { CommunicationMessages, EditorToRendererMessageType, RendererToEditorMessageType } from './rendering-message'
/**
* The communicator that is used to send messages from the editor to the renderer.
*/
export class EditorToRendererCommunicator extends WindowPostMessageCommunicator<
RendererToEditorMessageType,
EditorToRendererMessageType,
CommunicationMessages
> {
protected generateLogIdentifier(): string {
return 'E=>R'
}
}

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect } from 'react'
import { CommunicationMessages, RendererToEditorMessageType } from '../rendering-message'
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { Handler } from '../window-post-message-communicator'
/**
* Sets the handler for the given message type in the current editor to renderer communicator.
*
* @param messageType The message type that should be used to listen to.
* @param handler The handler that should be called if a message with the given message type was received.
*/
export const useEditorReceiveHandler = <R extends RendererToEditorMessageType>(
messageType: R,
handler: Handler<CommunicationMessages, R>
): void => {
const editorToRendererCommunicator = useEditorToRendererCommunicator()
useEffect(() => {
editorToRendererCommunicator.setHandler(messageType, handler)
return () => {
editorToRendererCommunicator.setHandler(messageType, undefined)
}
}, [editorToRendererCommunicator, handler, messageType])
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect } from 'react'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
/**
* Executes the given callback if it changes or the renderer is ready for receiving messages.
*
* @param sendOnReadyCallback The callback that should get executed.
*/
export const useEffectOnRendererReady = (sendOnReadyCallback: () => void): void => {
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
useEffect(() => {
if (rendererReady) {
sendOnReadyCallback()
}
}, [rendererReady, sendOnReadyCallback])
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../hooks/common/use-application-state'
/**
* Returns the current ready status of the renderer.
*/
export const useIsRendererReady = (): boolean => useApplicationState((state) => state.rendererStatus.rendererReady)

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect } from 'react'
import { CommunicationMessages, EditorToRendererMessageType } from '../rendering-message'
import { Handler } from '../window-post-message-communicator'
import { useRendererToEditorCommunicator } from '../../../editor-page/render-context/renderer-to-editor-communicator-context-provider'
/**
* Sets the handler for the given message type in the current renderer to editor communicator.
*
* @param messageType The message type that should be used to listen to.
* @param handler The handler that should be called if a message with the given message type was received.
*/
export const useRendererReceiveHandler = <MESSAGE_TYPE extends EditorToRendererMessageType>(
messageType: MESSAGE_TYPE,
handler: Handler<CommunicationMessages, MESSAGE_TYPE>
): void => {
const editorToRendererCommunicator = useRendererToEditorCommunicator()
useEffect(() => {
editorToRendererCommunicator.setHandler(messageType, handler)
return () => {
editorToRendererCommunicator.setHandler(messageType, undefined)
}
}, [editorToRendererCommunicator, handler, messageType])
}

View file

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useCallback } from 'react'
import { CommunicationMessages, EditorToRendererMessageType } from '../rendering-message'
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { PostMessage } from '../window-post-message-communicator'
import { useEffectOnRendererReady } from './use-effect-on-renderer-ready'
export const useSendToRenderer = (
message: undefined | Extract<CommunicationMessages, PostMessage<EditorToRendererMessageType>>
): void => {
const iframeCommunicator = useEditorToRendererCommunicator()
useEffectOnRendererReady(
useCallback(() => {
if (message) {
iframeCommunicator.sendMessageToOtherSide(message)
}
}, [iframeCommunicator, message])
)
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { WindowPostMessageCommunicator } from './window-post-message-communicator'
import { CommunicationMessages, EditorToRendererMessageType, RendererToEditorMessageType } from './rendering-message'
/**
* The communicator that is used to send messages from the renderer to the editor.
*/
export class RendererToEditorCommunicator extends WindowPostMessageCommunicator<
EditorToRendererMessageType,
RendererToEditorMessageType,
CommunicationMessages
> {
protected generateLogIdentifier(): string {
return 'E<=R'
}
}

View file

@ -0,0 +1,131 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
import { RendererFrontmatterInfo } from '../../common/note-frontmatter/types'
export enum CommunicationMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
RENDERER_READY = 'RENDERER_READY',
SET_DARKMODE = 'SET_DARKMODE',
ON_TASK_CHECKBOX_CHANGE = 'ON_TASK_CHECKBOX_CHANGE',
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
IMAGE_CLICKED = 'IMAGE_CLICKED',
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
GET_WORD_COUNT = 'GET_WORD_COUNT',
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
SET_FRONTMATTER_INFO = 'SET_FRONTMATTER_INFO'
}
export interface NoPayloadMessage {
type: CommunicationMessageType.RENDERER_READY | CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
}
export interface SetDarkModeMessage {
type: CommunicationMessageType.SET_DARKMODE
activated: boolean
}
export interface ImageDetails {
alt?: string
src: string
title?: string
}
export interface SetBaseUrlMessage {
type: CommunicationMessageType.SET_BASE_CONFIGURATION
baseConfiguration: BaseConfiguration
}
export interface GetWordCountMessage {
type: CommunicationMessageType.GET_WORD_COUNT
}
export interface ImageClickedMessage {
type: CommunicationMessageType.IMAGE_CLICKED
details: ImageDetails
}
export interface SetMarkdownContentMessage {
type: CommunicationMessageType.SET_MARKDOWN_CONTENT
content: string
}
export interface SetScrollStateMessage {
type: CommunicationMessageType.SET_SCROLL_STATE
scrollState: ScrollState
}
export interface OnTaskCheckboxChangeMessage {
type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
lineInMarkdown: number
checked: boolean
}
export interface OnFirstHeadingChangeMessage {
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
firstHeading: string | undefined
}
export interface SetFrontmatterInfoMessage {
type: CommunicationMessageType.SET_FRONTMATTER_INFO
frontmatterInfo: RendererFrontmatterInfo
}
export interface OnHeightChangeMessage {
type: CommunicationMessageType.ON_HEIGHT_CHANGE
height: number
}
export interface OnWordCountCalculatedMessage {
type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED
words: number
}
export type CommunicationMessages =
| NoPayloadMessage
| SetDarkModeMessage
| SetBaseUrlMessage
| GetWordCountMessage
| ImageClickedMessage
| SetMarkdownContentMessage
| SetScrollStateMessage
| OnTaskCheckboxChangeMessage
| OnFirstHeadingChangeMessage
| SetFrontmatterInfoMessage
| OnHeightChangeMessage
| OnWordCountCalculatedMessage
export type EditorToRendererMessageType =
| CommunicationMessageType.SET_MARKDOWN_CONTENT
| CommunicationMessageType.SET_DARKMODE
| CommunicationMessageType.SET_SCROLL_STATE
| CommunicationMessageType.SET_BASE_CONFIGURATION
| CommunicationMessageType.GET_WORD_COUNT
| CommunicationMessageType.SET_FRONTMATTER_INFO
export type RendererToEditorMessageType =
| CommunicationMessageType.RENDERER_READY
| CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
| CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
| CommunicationMessageType.SET_SCROLL_STATE
| CommunicationMessageType.IMAGE_CLICKED
| CommunicationMessageType.ON_HEIGHT_CHANGE
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
export enum RendererType {
DOCUMENT,
INTRO,
SLIDESHOW
}
export interface BaseConfiguration {
baseUrl: string
rendererType: RendererType
}

View file

@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Error that will be thrown if a message couldn't be sent.
*/
export class IframeCommunicatorSendingError extends Error {}
export type Handler<MESSAGES, MESSAGE_TYPE extends string> =
| ((values: Extract<MESSAGES, PostMessage<MESSAGE_TYPE>>) => void)
| undefined
export type HandlerMap<MESSAGES, MESSAGE_TYPE extends string> = Partial<{
[key in MESSAGE_TYPE]: Handler<MESSAGES, MESSAGE_TYPE>
}>
export interface PostMessage<MESSAGE_TYPE extends string> {
type: MESSAGE_TYPE
}
/**
* Base class for communication between renderer and editor.
*/
export abstract class WindowPostMessageCommunicator<
RECEIVE_TYPE extends string,
SEND_TYPE extends string,
MESSAGES extends PostMessage<RECEIVE_TYPE | SEND_TYPE>
> {
private messageTarget?: Window
private targetOrigin?: string
private communicationEnabled: boolean
private handlers: HandlerMap<MESSAGES, RECEIVE_TYPE> = {}
constructor() {
window.addEventListener('message', this.handleEvent.bind(this))
this.communicationEnabled = false
}
/**
* Removes the message event listener from the {@link window}
*/
public unregisterEventListener(): void {
window.removeEventListener('message', this.handleEvent.bind(this))
}
/**
* Sets the target for message sending.
* Messages can be sent as soon as the communication is enabled.
*
* @see enableCommunication
* @param otherSide The target {@link Window} that should receive the messages.
* @param otherOrigin The origin from the URL of the target. If this isn't correct then the message sending will produce CORS errors.
*/
public setMessageTarget(otherSide: Window, otherOrigin: string): void {
this.messageTarget = otherSide
this.targetOrigin = otherOrigin
this.communicationEnabled = false
}
/**
* Unsets the message target. Should be used if the old target isn't available anymore.
*/
public unsetMessageTarget(): void {
this.messageTarget = undefined
this.targetOrigin = undefined
this.communicationEnabled = false
}
/**
* Enables the message communication.
* Should be called as soon as the other sides is ready to receive messages.
*/
public enableCommunication(): void {
this.communicationEnabled = true
}
/**
* Sends a message to the message target.
*
* @param message The message to send.
*/
public sendMessageToOtherSide(message: Extract<MESSAGES, PostMessage<SEND_TYPE>>): void {
if (this.messageTarget === undefined || this.targetOrigin === undefined) {
throw new IframeCommunicatorSendingError(`Other side is not set.\nMessage was: ${JSON.stringify(message)}`)
}
if (!this.communicationEnabled) {
throw new IframeCommunicatorSendingError(
`Communication isn't enabled. Maybe the other side is not ready?\nMessage was: ${JSON.stringify(message)}`
)
}
console.debug('[WPMC ' + this.generateLogIdentifier() + '] Sent event', message)
this.messageTarget.postMessage(message, this.targetOrigin)
}
/**
* Sets the handler method that processes messages with the given message type.
* If there is already a handler for the given message type then the handler will be overwritten.
*
* @param messageType The message type for which the handler should be called
* @param handler The handler that processes messages with the given message type.
*/
public setHandler<R extends RECEIVE_TYPE>(messageType: R, handler: Handler<MESSAGES, R>): void {
this.handlers[messageType] = handler as Handler<MESSAGES, RECEIVE_TYPE>
}
/**
* Generates a unique identifier that helps to separate log messages in the console from different communicators.
* @return the identifier
*/
protected abstract generateLogIdentifier(): string
/**
* Receives the message events and calls the handler that is mapped to the correct type.
*
* @param event The received event
* @return {@code true} if the event was processed.
*/
protected handleEvent(event: MessageEvent<PostMessage<RECEIVE_TYPE>>): boolean | undefined {
const data = event.data
const handler = this.handlers[data.type]
if (!handler) {
return true
}
console.debug('[WPMC ' + this.generateLogIdentifier() + '] Received event ', data)
handler(data as Extract<MESSAGES, PostMessage<RECEIVE_TYPE>>)
return false
}
}