You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +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: [
|
presets: [
|
||||||
[
|
[
|
||||||
'classic',
|
'classic',
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@docusaurus/core": "2.4.3",
|
"@docusaurus/core": "2.4.3",
|
||||||
"@docusaurus/plugin-client-redirects": "2.4.3",
|
"@docusaurus/plugin-client-redirects": "2.4.3",
|
||||||
"@docusaurus/preset-classic": "2.4.3",
|
"@docusaurus/preset-classic": "2.4.3",
|
||||||
|
"@docusaurus/theme-mermaid": "2.4.3",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"prism-react-renderer": "^1.3.5",
|
"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
|
## 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.
|
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:
|
For example, let's say we define a command in the plugin:
|
||||||
@ -100,4 +102,214 @@ window.addEventListener('message', ((event) => {
|
|||||||
eventHandlers[eventId](...eventArgs);
|
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
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 6.0.4
|
||||||
resolution: "@braintree/sanitize-url@npm:6.0.4"
|
resolution: "@braintree/sanitize-url@npm:6.0.4"
|
||||||
checksum: f5ec6048973722ea1c46ae555d2e9eb848d7fa258994f8ea7d6db9514ee754ea3ef344ef71b3696d486776bcb839f3124e79f67c6b5b2814ed2da220b340627c
|
checksum: f5ec6048973722ea1c46ae555d2e9eb848d7fa258994f8ea7d6db9514ee754ea3ef344ef71b3696d486776bcb839f3124e79f67c6b5b2814ed2da220b340627c
|
||||||
@ -5187,6 +5187,25 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@docusaurus/theme-search-algolia@npm:2.4.3":
|
||||||
version: 2.4.3
|
version: 2.4.3
|
||||||
resolution: "@docusaurus/theme-search-algolia@npm: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/module-type-aliases": 2.4.3
|
||||||
"@docusaurus/plugin-client-redirects": 2.4.3
|
"@docusaurus/plugin-client-redirects": 2.4.3
|
||||||
"@docusaurus/preset-classic": 2.4.3
|
"@docusaurus/preset-classic": 2.4.3
|
||||||
|
"@docusaurus/theme-mermaid": 2.4.3
|
||||||
"@fortawesome/fontawesome-svg-core": 6.4.2
|
"@fortawesome/fontawesome-svg-core": 6.4.2
|
||||||
"@fortawesome/free-brands-svg-icons": 6.4.2
|
"@fortawesome/free-brands-svg-icons": 6.4.2
|
||||||
"@fortawesome/free-regular-svg-icons": 6.4.2
|
"@fortawesome/free-regular-svg-icons": 6.4.2
|
||||||
@ -17881,6 +17901,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"damerau-levenshtein@npm:^1.0.8":
|
||||||
version: 1.0.8
|
version: 1.0.8
|
||||||
resolution: "damerau-levenshtein@npm:1.0.8"
|
resolution: "damerau-levenshtein@npm:1.0.8"
|
||||||
@ -19190,6 +19220,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"dompurify@npm:^3.0.5":
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
resolution: "dompurify@npm:3.0.5"
|
resolution: "dompurify@npm:3.0.5"
|
||||||
@ -29210,6 +29247,30 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"methods@npm:^1.1.2, methods@npm:~1.1.2":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "methods@npm:1.1.2"
|
resolution: "methods@npm:1.1.2"
|
||||||
@ -39798,6 +39859,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"stylis@npm:^4.1.3":
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
resolution: "stylis@npm:4.3.0"
|
resolution: "stylis@npm:4.3.0"
|
||||||
|
Reference in New Issue
Block a user