You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Chore: Add RemoteMessenger documentation to plugin technical spec (#10112)
This commit is contained in:
		| @@ -149,6 +149,11 @@ const config = { | ||||
| 		], | ||||
| 	], | ||||
|  | ||||
| 	markdown: { | ||||
| 		mermaid: true, | ||||
| 	}, | ||||
| 	themes: ['@docusaurus/theme-mermaid'], | ||||
|  | ||||
| 	presets: [ | ||||
| 		[ | ||||
| 			'classic', | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
|     "@docusaurus/core": "2.4.3", | ||||
|     "@docusaurus/plugin-client-redirects": "2.4.3", | ||||
|     "@docusaurus/preset-classic": "2.4.3", | ||||
|     "@docusaurus/theme-mermaid": "2.4.3", | ||||
|     "@mdx-js/react": "^1.6.22", | ||||
|     "clsx": "^1.2.1", | ||||
|     "prism-react-renderer": "^1.3.5", | ||||
|   | ||||
| @@ -32,6 +32,8 @@ The plugin API is a light wrapper over Joplin's internal functions and services. | ||||
|  | ||||
| ## Handling events between the plugin and the host | ||||
|  | ||||
| ### On Desktop | ||||
|  | ||||
| Handling events in plugins is relatively complicated due to the need to send IPC messages and the limitations of the IPC protocol, which in particular cannot transfer functions. | ||||
|  | ||||
| For example, let's say we define a command in the plugin: | ||||
| @@ -100,4 +102,214 @@ window.addEventListener('message', ((event) => { | ||||
| 		eventHandlers[eventId](...eventArgs); | ||||
| 	}	 | ||||
| })); | ||||
| ``` | ||||
| ``` | ||||
|  | ||||
| ### On Mobile | ||||
|  | ||||
| On mobile, not only is the main plugin script running in a separate process, but so are the note editor, renderer, and dialogs. | ||||
|  | ||||
| To simplify communication between these processes, a `RemoteMessenger` class is introduced. | ||||
|  | ||||
| `RemoteMessenger` is `abstract` and independent from how messages are sent. Each type of message channel should have a subclass of `RemoteMessenger` to handle communication over that channel type. For example, `WebViewToRNMessenger` handles communication with React Native from within a React Native WebView. Similarly, `RNToWebViewMessenger` handles communication with a React Native WebView from within React Native. | ||||
|  | ||||
| #### The `RemoteMessenger<LocalInterface, RemoteInterface>` class | ||||
|  | ||||
| The `RemoteMessenger` class simplifies communication over `postMessage`. Its job is to convert asynchronous method calls to messages, then send these messages to another `RemoteMessenger` that handles them. | ||||
|  | ||||
| ```mermaid | ||||
| flowchart | ||||
| 	RemoteMessenger1<--postMessage-->RemoteMessenger2 | ||||
| ``` | ||||
|  | ||||
| For example, if we have | ||||
| ```typescript | ||||
| // Dialogs | ||||
| export interface MainProcessApi { | ||||
| 	onSubmit: ()=> void; | ||||
| 	onDismiss: ()=> void; | ||||
| 	onError: (message: string)=> Promise<void>; | ||||
| } | ||||
|  | ||||
| export interface WebViewApi { | ||||
| 	setCss: (css: string)=> void; | ||||
| 	closeDialog: ()=> Promise<void>; | ||||
| 	setButtons: (buttons: ButtonSpec[])=> void; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| We might then create messengers like this: | ||||
|  | ||||
| **In the WebView:** | ||||
| ```typescript | ||||
| const webViewApiImpl: WebViewApi = { | ||||
| 	// ... some implementation here ... | ||||
| 	setCss: css => {} // ... | ||||
| }; | ||||
|  | ||||
| // Different messageChannelIds allow us to have multiple messengers communicate over the same channel. | ||||
| // Different IDs prevent the wrong messenger from acting on a message. | ||||
| const messageChannelId = 'test-channel'; | ||||
|  | ||||
| const messenger = new WebViewToRNMessenger<WebViewApi, MainProcessApi>(messageChannelId, webViewApiImpl); | ||||
| ``` | ||||
|  | ||||
|  | ||||
| **In the main process:** | ||||
| ```typescript | ||||
| const mainProcessApiImpl: WebViewApi = { | ||||
| 	// ... some implementation here ... | ||||
| 	closeDialog: () => {} // ... | ||||
| }; | ||||
|  | ||||
| const messageChannelId = 'test-channel'; | ||||
| const messenger = new WebViewToRNMessenger<MainProcessApi, WebViewApi>(messageChannelId, mainProcessApiImpl); | ||||
|  | ||||
| // We can now use the messenger. | ||||
| // Messages are all asynchronous. | ||||
| await messenger.remoteApi.setCss('* { color: red; }'); | ||||
| ``` | ||||
|  | ||||
| To call `messenger.remoteApi.setCss(...)`, we use a process similar to the following: | ||||
|  | ||||
| ##### First: Queue the method call and wait for both messengers to be ready.  | ||||
|  | ||||
| To avoid sending messages that won't be received (and waiting indefinitely for a response), `RemoteMessenger` buffers messages until it receives a `RemoteReady` event. | ||||
|  | ||||
| When a messenger is ready, it sends a message with `kind: RemoteReady`. | ||||
| ```mermaid | ||||
| flowchart | ||||
| 	postMessage1(["postMessage({ kind: RemoteReady, ... })"]) | ||||
| 	rm1--1-->postMessage1--2-->rm2 | ||||
| 	subgraph MainProcess | ||||
| 		rm1["m1 = RemoteMessenger< MainProcessApi,WebViewApi >"] | ||||
| 	end | ||||
| 	subgraph WebView | ||||
| 		rm2["RemoteMessenger< WebViewApi,MainProcessApi >"] | ||||
| 	end | ||||
| ``` | ||||
|  | ||||
| When a messenger receives a message with `kind: RemoteReady`, it replies with the same message type. | ||||
| ```mermaid | ||||
| flowchart | ||||
| 	postMessage1(["postMessage({ kind: RemoteReady, ... })"]) | ||||
| 	rm2--3-->postMessage1--4-->rm1 | ||||
| 	subgraph MainProcess | ||||
| 		rm1["m1 = RemoteMessenger< MainProcessApi,WebViewApi >"] | ||||
| 	end | ||||
| 	subgraph WebView | ||||
| 		rm2["RemoteMessenger< WebViewApi,MainProcessApi >"] | ||||
| 	end | ||||
| ``` | ||||
|  | ||||
| ##### Second: Send all queued messages | ||||
|  | ||||
| After both messengers are ready, we wend all queued messages. In this case, that's the `setCss` message: | ||||
| ```typescript | ||||
| { | ||||
| 	kind: MessageType.InvokeMethod, | ||||
| 	methodPath: ['setCss'], | ||||
| 	arguments: { | ||||
| 		serializable: ['* { color: red; }'], | ||||
|  | ||||
| 		// If there were callbacks, we would assign them | ||||
| 		// IDs and send the IDs here. | ||||
| 		callbacks: [ null ], | ||||
| 	}, | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ```mermaid | ||||
| flowchart | ||||
| 	postMessage(["postMessage({ kind: InvokeMethod, ... })"]) | ||||
| 	rm1--2-->postMessage--3-->rm2 | ||||
| 	subgraph MainProcess | ||||
| 		call(["await m1.remoteApi.setCss('...')"]) | ||||
| 		call--1-->rm1 | ||||
| 		rm1["m1 = RemoteMessenger< MainProcessApi,WebViewApi >"] | ||||
| 	end | ||||
| 	subgraph WebView | ||||
| 		rm2["RemoteMessenger< WebViewApi,MainProcessApi >"] | ||||
| 		webViewApiImpl["webViewApiImpl.setCss"] | ||||
| 		rm2--4-->webViewApiImpl | ||||
| 	end | ||||
| ``` | ||||
|  | ||||
| After handling the message, a result is returned also by `postMessage`, this time with the `kind` `ReturnValueResponse`: | ||||
|  | ||||
| ```mermaid | ||||
| flowchart | ||||
| 	postMessage(["postMessage({ kind: ReturnValueResponse, ... })"]) | ||||
| 	rm2--6-->postMessage--7-->rm1 | ||||
| 	subgraph WebView | ||||
| 		rm2["RemoteMessenger< WebViewApi,MainProcessApi >"] | ||||
| 		webViewApiImpl["webViewApiImpl.setCss"] | ||||
| 		webViewApiImpl--5-->rm2 | ||||
| 	end | ||||
| 	subgraph MainProcess | ||||
| 		rm1["m1 = RemoteMessenger< MainProcessApi,WebViewApi >"] | ||||
| 		call(["await m1.remoteApi.setCss('...')"]) | ||||
| 		rm1--8-->call | ||||
| 	end | ||||
| ``` | ||||
|  | ||||
| After receiving the response, the `setCss` call resolves. | ||||
|  | ||||
| On mobile, we address the same problem in similar, but more generalized way.  We define a `RemoteMessenger` class that handles `postMessage` communication. | ||||
|  | ||||
| #### `RemoteMessenger` and callbacks | ||||
|  | ||||
| Suppose we call a method in a way similar to the following: | ||||
| ```typescript | ||||
| messenger.remoteApi.joplin.plugins.register({ | ||||
| 	onStart: async () => { | ||||
| 		console.log('testing'); | ||||
| 	}, | ||||
| 	test: 'test', | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| We can't send callbacks over `postMessage`. As such, we assign the `onStart` callback an ID and send the ID instead. The message might look like this: | ||||
| ```typescript | ||||
| { | ||||
| 	kind: MessageType.InvokeMethod, | ||||
| 	methodPath: ['joplin', 'plugins', 'register'], | ||||
| 	arguments: { | ||||
| 		serializable: [ | ||||
| 			{ | ||||
| 				onStart: null, | ||||
| 				test: 'test', | ||||
| 			} | ||||
| 		], | ||||
| 		callbacks: [ | ||||
| 			{ | ||||
| 				onStart: 'some-generated-id-for-onStart', | ||||
| 				test: null, | ||||
| 			} | ||||
| 		], | ||||
| 	}, | ||||
| 	respondWithId: 'another-autogenerated-id', | ||||
| } | ||||
| ``` | ||||
|  | ||||
| **Note**: As before, the `respondWithId` connects a method call to its return value (the return value has the same ID). | ||||
|  | ||||
| The `arguments.callbacks` object contains **only** callback IDs and the `arguments.serializable` object contains **only** the serialisable arguments. The two objects otherwise should have the same structure. These two objects are merged by the `RemoteMessenger` that receives the message: | ||||
|  | ||||
| ```mermaid | ||||
| flowchart | ||||
| 	callbacks[arguments.callbacks] | ||||
| 	serializable[arguments.serializable] | ||||
|  | ||||
| 	callbacks--"only callbacks"-->original | ||||
| 	serializable--"only properties not in callbacks"-->original | ||||
| ``` | ||||
|  | ||||
| Callbacks are called by sending an `InvokeMethod` message similar to the following: | ||||
| ```typescript | ||||
| { | ||||
| 	kind: MessageType.InvokeMethod, | ||||
| 	methodPath: ['__callbacks', 'callback-id-here'], | ||||
| 	arguments: { ... }, | ||||
| 	respondWithId: 'some-autogenerated-id-here', | ||||
| } | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										70
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -3945,7 +3945,7 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "@braintree/sanitize-url@npm:^6.0.1": | ||||
| "@braintree/sanitize-url@npm:^6.0.0, @braintree/sanitize-url@npm:^6.0.1": | ||||
|   version: 6.0.4 | ||||
|   resolution: "@braintree/sanitize-url@npm:6.0.4" | ||||
|   checksum: f5ec6048973722ea1c46ae555d2e9eb848d7fa258994f8ea7d6db9514ee754ea3ef344ef71b3696d486776bcb839f3124e79f67c6b5b2814ed2da220b340627c | ||||
| @@ -5187,6 +5187,25 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "@docusaurus/theme-mermaid@npm:2.4.3": | ||||
|   version: 2.4.3 | ||||
|   resolution: "@docusaurus/theme-mermaid@npm:2.4.3" | ||||
|   dependencies: | ||||
|     "@docusaurus/core": 2.4.3 | ||||
|     "@docusaurus/module-type-aliases": 2.4.3 | ||||
|     "@docusaurus/theme-common": 2.4.3 | ||||
|     "@docusaurus/types": 2.4.3 | ||||
|     "@docusaurus/utils-validation": 2.4.3 | ||||
|     "@mdx-js/react": ^1.6.22 | ||||
|     mermaid: ^9.2.2 | ||||
|     tslib: ^2.4.0 | ||||
|   peerDependencies: | ||||
|     react: ^16.8.4 || ^17.0.0 | ||||
|     react-dom: ^16.8.4 || ^17.0.0 | ||||
|   checksum: 63b2eafaf929e3266d91b8c38bfa0aa9e4a6f625576d4c3c220426aaab3118185b2ed0d74fa359273e69c9f41dea3267d8ff77646acbcd1e1c3d392d20d8f77a | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "@docusaurus/theme-search-algolia@npm:2.4.3": | ||||
|   version: 2.4.3 | ||||
|   resolution: "@docusaurus/theme-search-algolia@npm:2.4.3" | ||||
| @@ -6736,6 +6755,7 @@ __metadata: | ||||
|     "@docusaurus/module-type-aliases": 2.4.3 | ||||
|     "@docusaurus/plugin-client-redirects": 2.4.3 | ||||
|     "@docusaurus/preset-classic": 2.4.3 | ||||
|     "@docusaurus/theme-mermaid": 2.4.3 | ||||
|     "@fortawesome/fontawesome-svg-core": 6.4.2 | ||||
|     "@fortawesome/free-brands-svg-icons": 6.4.2 | ||||
|     "@fortawesome/free-regular-svg-icons": 6.4.2 | ||||
| @@ -17881,6 +17901,16 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "dagre-d3-es@npm:7.0.9": | ||||
|   version: 7.0.9 | ||||
|   resolution: "dagre-d3-es@npm:7.0.9" | ||||
|   dependencies: | ||||
|     d3: ^7.8.2 | ||||
|     lodash-es: ^4.17.21 | ||||
|   checksum: 5f24ad9558e84066e70cfa6979320d93079979ac8b0a3b033e5330742aeeba74e205f66794ab6e0a82354b061a4e29c10a291590d7b2cf82b5780fab5443f5ba | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "damerau-levenshtein@npm:^1.0.8": | ||||
|   version: 1.0.8 | ||||
|   resolution: "damerau-levenshtein@npm:1.0.8" | ||||
| @@ -19190,6 +19220,13 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "dompurify@npm:2.4.3": | ||||
|   version: 2.4.3 | ||||
|   resolution: "dompurify@npm:2.4.3" | ||||
|   checksum: b440981f2a38cada2085759cc3d1e2f94571afc34343d011a8a6aa1ad91ae6abf651adbfa4994b0e2283f0ce81f7891cdb04b67d0b234c8d190cb70e9691f026 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "dompurify@npm:^3.0.5": | ||||
|   version: 3.0.5 | ||||
|   resolution: "dompurify@npm:3.0.5" | ||||
| @@ -29210,6 +29247,30 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "mermaid@npm:^9.2.2": | ||||
|   version: 9.4.3 | ||||
|   resolution: "mermaid@npm:9.4.3" | ||||
|   dependencies: | ||||
|     "@braintree/sanitize-url": ^6.0.0 | ||||
|     cytoscape: ^3.23.0 | ||||
|     cytoscape-cose-bilkent: ^4.1.0 | ||||
|     cytoscape-fcose: ^2.1.0 | ||||
|     d3: ^7.4.0 | ||||
|     dagre-d3-es: 7.0.9 | ||||
|     dayjs: ^1.11.7 | ||||
|     dompurify: 2.4.3 | ||||
|     elkjs: ^0.8.2 | ||||
|     khroma: ^2.0.0 | ||||
|     lodash-es: ^4.17.21 | ||||
|     non-layered-tidy-tree-layout: ^2.0.2 | ||||
|     stylis: ^4.1.2 | ||||
|     ts-dedent: ^2.2.0 | ||||
|     uuid: ^9.0.0 | ||||
|     web-worker: ^1.2.0 | ||||
|   checksum: 9e29177f289cc268ea4a2ca7a45ec0ca06f678007eae15a7cd54c682148a71367e861d2c9c0afa9f7474da154d9920524e59722186820e9bc0d79989305a7064 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "methods@npm:^1.1.2, methods@npm:~1.1.2": | ||||
|   version: 1.1.2 | ||||
|   resolution: "methods@npm:1.1.2" | ||||
| @@ -39798,6 +39859,13 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "stylis@npm:^4.1.2": | ||||
|   version: 4.3.1 | ||||
|   resolution: "stylis@npm:4.3.1" | ||||
|   checksum: d365f1b008677b2147e8391e9cf20094a4202a5f9789562e7d9d0a3bd6f0b3067d39e8fd17cce5323903a56f6c45388e3d839e9c0bb5a738c91726992b14966d | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "stylis@npm:^4.1.3": | ||||
|   version: 4.3.0 | ||||
|   resolution: "stylis@npm:4.3.0" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user