aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-01-01 13:40:29 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-01-01 13:54:32 +0100
commite8a4341a1cc4a93025355ca9d0a238f9263533e1 (patch)
tree5c62385b7693ddd221e0950215be8b2641bcfbdf
parentfeat: Load browserview contents asynchronously (diff)
downloadsophie-e8a4341a1cc4a93025355ca9d0a238f9263533e1.tar.gz
sophie-e8a4341a1cc4a93025355ca9d0a238f9263533e1.tar.zst
sophie-e8a4341a1cc4a93025355ca9d0a238f9263533e1.zip
docs: Add architecture docs
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--docs/architecture.md124
1 files changed, 124 insertions, 0 deletions
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..6467b6f
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,124 @@
1---
2title: Architecture
3---
4
5# Purpose
6
7Sophie is designed to
8
9* display _services_, i.e., web pages, most of which are messaging web applications,
10* allow the user to quickly switch between services and save resources by displaying multiple services in the same desktop applications,
11* provide integrations for services, e.g., to manage unread message counts and notification in a single place.
12
13Integrations for services are provided by pluggable _recipes_ to allow adding support for new services easily.
14A recipe may come directly from Sophie, a third party (under AGPLv3 licensing), or be created by the user themselves.
15
16# Electron
17
18Sophie is built on the [Electron](https://www.electronjs.org/) application framework, which in turn uses the [Chromium](https://www.chromium.org/) browser to execute javascript applications.
19
20## Displaying foreign web pages
21
22Electron makes two facilities readily available to display foreign web pages:
23
24* The easier solution that mimics the HTML `<iframe>` tag is [`<webview>`](https://www.electronjs.org/docs/latest/api/webview-tag/).
25 However, it is deprecated and has some [critical bugs](https://github.com/electron/electron/issues/25469) when loading content with modern web security headers.
26* The new solution is the [`BrowserView`](https://www.electronjs.org/docs/latest/api/browser-view/), which is the approach taken by Sophie.
27 The `BrowserView` is an overlay over the main browser window, so _Sophie will not be able render UI elements over foreign web pages_.
28 While this is somewhat limiting, it allows us to take advantage of modern Electron APIs and coding practices.
29
30## Processes and isolation
31
32An Electron application is split into a _main process_ and a number of _renderer processes_:
33
34* The main process has access to NodeJS APIs for direct file system access.
35 It is also the only process that can manipulate browser windows and `BrowserView` instances.
36 Therefore, most of Sophie's logic must be implemented in the main process.
37* The renderer processes are sandboxed processes responsible for rendering and executing web pages, both local and foreign.
38 In turn, they are split into an _isolated world_ and a _main world_.
39 The isolated world (world id 999) can use [IPC](https://www.electronjs.org/docs/latest/api/ipc-renderer/) to communicate with the main process.
40 The only way to expose APIs to the main world is through the [`contextBridge`](https://www.electronjs.org/docs/latest/api/context-bridge).
41 The main world (world id 0) then can execute untrusted code (e.g., foreign web pages) and still stay sandboxed.
42 In Sophie, both the main window user interface (a local web page) and services are loaded in isolated renderer processes.
43
44While not strictly necessary for local-only content, we use content isolation also for Sophie's UI to provide defense in depth against injection attacks.
45
46**Heads up:**
47The terminology here is a bit confusing.
48The _main process_ refers to the most privileged process of an Electron application.
49The _main world_ of a renderer process contains the code ran with the least privileges within an Electron application.
50
51**TODO:**
52We should figure out how recipes should fit into this model, and how much do we trust recipes.
53Since recipes interact with the corresponding service, a compromised service can always compromise the service's data.
54However, we should strive to minimize the compromise of other services' data by a compromised recipe.
55This suggest running inside a renderer process only, maybe even within an isolated context.
56
57# Data flow
58
59Sophie is a reactive application with unidirectional data flow for easy debugging.
60
61## Stores
62
63_Stores_ defined and maintained by [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree) act as a single source of truth.
64IPC messages that invoke store actions are validated by [https://github.com/colinhacks/zod] at context boundaries (both in the isolated world and in the main process).
65
66In the main process, the [`MainStore`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/main/src/stores/MainStore.ts) holds application state.
67
68Within the `MainStore`, changes to the [`SharedStore`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/shared/src/stores/SharedStore.ts) are pushed to the UI process.
69Thus it can hold application state relevant to displaying the UI.
70
71The [`Config`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/shared/src/stores/Config.ts) in the `SharedStore` holds the application configuration, including the list of services to be displayed.
72It is synchronized with the `config.json5` file in user data directory, which should be human-readable and -editable to facilitate debugging and other advanced use cases.
73
74In the UI renderer process, the [`RendererStore`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/renderer/src/stores/RendererStore.ts) hold the UI sate.
75It contains a read-only copy of the `SharedStore`.
76Any actions of the `RendererStore` that should affect the shared state have to go through the IP API exposed from the isolated world of the UI renderer process.
77
78To reduce the amount of code injected into service frames, service renderer processes contain no stores.
79Instead, they purely rely on IPC messages to invoke actions in the main process (but they are not notified of the result).
80
81## Controllers
82
83In the main process, _controllers_ react to `MainStore` changes by invoking Electron APIs and subscribe to Electron events in order to invoke `MainStore` actions.
84
85For easier testability, controllers may rely on _services_ abstracting away the underlying Electron APIs.
86The service has to come with a TypeScript interface and an implementation.
87In the tests, the default implementation of the interface is replaced by a mock.
88
89The services and controllers are instatiated and connected to the `MainStore` in the [composition root](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/main/src/compositionRoot.ts).
90
91**TODO:**
92While a service is a common term in MVC application architecture, we should come up with a different name to avoid clashing witch services, i.e., web sites loaded by Sophie.
93
94## React
95
96In the UI renderer process, the UI is reactively derived from the `RendererStore` via [mobx-react-lite](https://github.com/mobxjs/mobx) and [React](https://reactjs.org/).
97We use the [MUI](https://mui.com/) UI library for UI components.
98
99In particular, the [`BrowserViewPlaceholder`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/renderer/src/components/BrowserViewPlaceholder.tsx) synchronizes the location where the service `BrowserView` should be renderer to the main process.
100We must take care not to render anything in this area, since it will be entirely covered by the service.
101
102# Packages
103
104The code of Sophie is distirbuted between different Node packages according to how they are loaded into the application.
105
106All packages except the renderer package are tree-shaken and bundled with [esbuild](https://esbuild.github.io/) for quicker loading.
107The web application in the renderer packages is tree-shaken and bundled with [vite](https://vitejs.dev/).
108
109<dl>
110 <dt><a href="https://gitlab.com/say-hi-to-sophie/sophie/-/tree/main/packages/main">main</a></dt>
111 <dd> Contains the code running in the main process. Bundled into a CommonJS package with external dependencies on Electron APIs.</dd>
112 <dt><a href="https://gitlab.com/say-hi-to-sophie/sophie/-/tree/main/packages/preload">preload</a></dt>
113 <dd>Exposes the IPC APIs for the renderer package from the isolated world of the UI renderer process. Bundled into a CommonJS package with external dependencies on the Electron IPC API.</dd>
114 <dt><a href="https://gitlab.com/say-hi-to-sophie/sophie/-/tree/main/packages/renderer">renderer</a></dt>
115 <dd> Contains the user interface of Sophie running in the main world of the UI renderer process.</dd>
116 <dt><a href="https://gitlab.com/say-hi-to-sophie/sophie/-/tree/main/packages/service-inject">service-inject</a></dt>
117 <dd>Injected into the main world of the service renderer process to shim browser APIs, such as screen sharing (overwriting properties of the <code>window</code> object requires running in the main world). Bundled into an IIFE (immediately invoked function expression) to avoid clashing with the service's code.</dd>
118 <dt><a href="https://gitlab.com/say-hi-to-sophie/sophie/-/tree/main/packages/service-preload">service-preload</a></dt>
119 <dd>Exposes the IPC APIs for recipes and the service-inject package from the isolated world of the service renderer process. Bundled into a CommonJS package with external dependencies on the Electron IPC API.</dd>
120 <dt><a href="https://gitlab.com/say-hi-to-sophie/sophie/-/tree/main/packages/service-shared">service-shared</a></dt>
121 <dd>IPC message definitions shared between the main and service-preload packages. During build, it gets bundled into an ESM package with all dependencies external, and is used within the build of dependent packages.</dd>
122 <dt><a href="https://gitlab.com/say-hi-to-sophie/sophie/-/tree/main/packages/shared">shared</a></dt>
123 <dd>Store and IPC message definitions shared between the main, preload, and renderer packages. During build, it gets bundled into an ESM package with all dependencies external, and is used within the build of dependent packages.</dd>
124</dl>