mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Chore: Add RemoteMessenger documentation to plugin technical spec (#10112)
This commit is contained in:
parent
3e34f150b8
commit
4ac0cdf556
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user