1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-19 20:31:46 +02:00

Chore: Add RemoteMessenger documentation to plugin technical spec (#10112)

This commit is contained in:
Henry Heino 2024-03-14 11:39:27 -07:00 committed by GitHub
parent 3e34f150b8
commit 4ac0cdf556
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 288 additions and 2 deletions

View File

@ -149,6 +149,11 @@ const config = {
],
],
markdown: {
mermaid: true,
},
themes: ['@docusaurus/theme-mermaid'],
presets: [
[
'classic',

View File

@ -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",

View File

@ -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',
}
```

View File

@ -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"