|
1 | | -# Main Process Architecture |
2 | | - |
3 | | -This document describes the architecture and maintenance guidelines for the Electron main process located in `src/main`. |
4 | | - |
5 | | -It is intentionally written as a **practical guide** for how this repository structures main-process code (startup, windows, IPC, storage, services, and feature modules). |
6 | | - |
7 | | -## Architecture Goals |
8 | | - |
9 | | -- **Feature modularity**: each feature lives in its own folder (`auth/`, `user/`, `updater/`, etc.) and exposes a small surface area to the app entrypoint. |
10 | | -- **Single IPC gateway**: renderer talks to main through a single exposed API (`window.electron`) and two IPC channels (`send`, `invoke`) plus one receive channel (`receive`). |
11 | | -- **Window lifecycle control**: a shared window controller (`@shared/control-window/*`) creates, caches, reuses, and hides windows consistently. |
12 | | -- **Security by default**: IPC handlers validate the sender frame (`@shared/utils.ts`) to reduce the chance of unauthorized IPC calls. |
13 | | -- **Shared infrastructure**: storage, REST API wrapper, menu, tray, notifications, and logging are centralized under `@shared/`. |
14 | | - |
15 | | -## Directory Structure |
16 | | - |
17 | | -The main process code is organized by feature, with a shared directory for common utilities. |
18 | | - |
19 | | -```architecture |
20 | | -src/main/ |
21 | | -├── @shared/ # Shared utilities, types, and helpers |
22 | | -│ ├── control-window/ # Window creation/destruction logic |
23 | | -│ ├── ipc/ # IPC gateway registration + helpers |
24 | | -│ ├── menu/ # Application menu configuration |
25 | | -│ ├── services/ # Shared services (e.g., REST API, error handling) |
26 | | -│ ├── tray/ # System tray logic |
27 | | -│ ├── logger.ts # Electron-log wrapper |
28 | | -│ ├── notification.ts # Shared Notification instance |
29 | | -│ ├── path-resolver.ts # Resolve preload/UI/assets paths |
30 | | -│ ├── store.ts # Electron store wrapper |
31 | | -│ └── utils.ts # General utilities (IPC wrappers, env checks) |
32 | | -├── <module>/ # Modules (e.g., auth, user, updater) |
33 | | -│ ├── ipc.ts # IPC event handlers registration |
34 | | -│ ├── service.ts # Business logic and external API calls |
35 | | -│ ├── utils.ts # Helper functions of module |
36 | | -│ ├── types.ts # Feature-specific types |
37 | | -│ └── window.ts # Window configuration (if applicable) |
38 | | -├── app.ts # Application entry point |
39 | | -├── config.ts # Global configuration (URLs, window names) |
40 | | -└── preload.cts # Context bridge and preload script |
41 | | -``` |
42 | | - |
43 | | -## Startup Flow (What happens on app launch) |
44 | | - |
45 | | -The main process entrypoint is `src/main/app.ts`. |
46 | | - |
47 | | -High-level flow: |
48 | | - |
49 | | -1. Load environment variables (special handling in production builds). |
50 | | -2. Apply global Electron settings (e.g., hardware acceleration disabled). |
51 | | -3. Configure updater feed (Windows), crash handlers, and global UI infra (tray/menu/notifications). |
52 | | -4. Create the main window using the shared window factory. |
53 | | -5. Register IPC once and dispatch events to feature modules. |
54 | | - |
55 | | -### Environment loading |
56 | | - |
57 | | -Both `src/main/app.ts` and `src/main/config.ts` load `.env` in production from `process.resourcesPath/.env`. This keeps config available when packaged. |
58 | | - |
59 | | -### Global configuration (`config.ts`) |
60 | | - |
61 | | -`src/main/config.ts` centralizes: |
62 | | - |
63 | | -- window hashes (`windows`) |
64 | | -- folders used by packaging/runtime (`folders`) |
65 | | -- menu labels and icon names |
66 | | -- copy for notifications/errors (`messages`) |
67 | | -- publish metadata and REST API URLs (`publishOptions`, `restApi`) |
68 | | - |
69 | | -Feature modules should import constants from `config.ts` instead of hardcoding strings. |
70 | | - |
71 | | -## IPC Architecture |
72 | | - |
73 | | -### Renderer API surface (`preload.cts`) |
74 | | - |
75 | | -The preload script exposes a strict API under `window.electron`: |
76 | | - |
77 | | -- `send(payload)` → fire-and-forget events to main (IPC channel: `send`) |
78 | | -- `invoke(payload)` → request/response to main (IPC channel: `invoke`) |
79 | | -- `receive(callback)` → subscribe to main → renderer push messages (IPC channel: `receive`) |
80 | | - |
81 | | -This repo uses typed envelopes (see `@shared/ipc/types.ts`) where every message has a `type` and optional `data`. |
82 | | - |
83 | | -### Main IPC gateway (`@shared/ipc/ipc.ts`) |
84 | | - |
85 | | -Main process listens on exactly two inbound channels: |
86 | | - |
87 | | -- `ipcMain.on("send", ...)` |
88 | | -- `ipcMain.handle("invoke", ...)` |
89 | | - |
90 | | -And it emits to renderer using a single outbound channel: |
91 | | - |
92 | | -- `webContents.send("receive", payload)` |
93 | | - |
94 | | -Helpers: |
95 | | - |
96 | | -- `sendToRenderer(webContents, payload)` |
97 | | -- `replyToRenderer(event, payload)` |
98 | | - |
99 | | -### IPC security: sender validation (`@shared/utils.ts`) |
100 | | - |
101 | | -Before dispatching, every inbound IPC message validates the sender frame via `validateEventFrame(event.senderFrame)`. |
102 | | - |
103 | | -Rules (simplified): |
104 | | - |
105 | | -- In dev, allow `localhost:<LOCALHOST_PORT|LOCALHOST_ELECTRON_SERVER_PORT>`. |
106 | | -- In prod, require `file:` URLs and only accept known window hashes (from `config.ts -> windows`). |
107 | | - |
108 | | -When adding new windows/routes, ensure their hash exists in `config.ts -> windows`, otherwise IPC will reject events. |
109 | | - |
110 | | -### IPC dispatch pattern in `app.ts` |
111 | | - |
112 | | -`src/main/app.ts` registers IPC once and delegates per feature: |
113 | | - |
114 | | -- `onSend`: fan-out to feature `handleSend` functions (`auth`, `user`, `app-preload`, `updater`) |
115 | | -- `onInvoke`: delegate to the single invoke handler currently used (`app-version`) |
116 | | - |
117 | | -If you add a new feature with IPC, follow the same pattern: export `handleSend` and/or `handleInvoke` from the feature module and register it in `app.ts`. |
118 | | - |
119 | | -## Window Architecture |
120 | | - |
121 | | -### Window creation (`@shared/control-window/create.ts`) |
122 | | - |
123 | | -All windows should be created through `createWindow(...)`. It standardizes: |
124 | | - |
125 | | -- preload script location (points to built `dist-main/preload.cjs`) |
126 | | -- dev vs prod URL loading: |
127 | | - - dev → `http://localhost:<LOCALHOST_PORT>/#<hash>` |
128 | | - - prod → `loadFile(dist-renderer/index.html, { hash })` |
129 | | -- optional direct `loadURL` (used for OAuth and the preload spinner window) |
130 | | -- basic security defaults: |
131 | | - - `contextIsolation: true` |
132 | | - - `nodeIntegration: false` |
133 | | - |
134 | | -### Window caching and reuse (`@shared/control-window/cache.ts`, `receive.ts`) |
135 | | - |
136 | | -Windows can be cached by `hash`: |
137 | | - |
138 | | -- If `isCache: true` and a cached instance exists, `createWindow` reuses it and calls `show()`. |
139 | | -- Cached windows are hidden on close (`event.preventDefault(); window.hide()`), which enables “re-open without re-create”. |
140 | | - |
141 | | -Use `getWindow(hash)` to fetch cached windows safely (returns `undefined` if destroyed). |
142 | | - |
143 | | -### Global window cleanup (`@shared/control-window/destroy.ts`) |
144 | | - |
145 | | -`destroyWindows()` destroys all windows on `before-quit`. |
146 | | - |
147 | | -### Content Security Policy |
148 | | - |
149 | | -`createWindow` attaches a CSP header (when `isCache` is enabled and not using `loadURL`). It optionally allows connecting to `BASE_REST_API` and allows `unsafe-inline` scripts only in dev. |
150 | | - |
151 | | -If you add a window that needs network access, prefer routing those requests through the shared REST API service and keep CSP consistent. |
152 | | - |
153 | | -## Storage and Caching |
154 | | - |
155 | | -### In-memory store vs persistent storage (`@shared/store.ts`) |
156 | | - |
157 | | -This repo uses two storage layers: |
158 | | - |
159 | | -- `store` (in-memory `Map`) for runtime references/flags (e.g., update process flag, window references) |
160 | | -- `electron-store` for persistence (auth token, user id, cached API responses) |
161 | | - |
162 | | -Key patterns: |
163 | | - |
164 | | -- `setStore("updateWindow", window)` stores an in-memory reference. |
165 | | -- `setElectronStorage("authToken", token)` persists auth session. |
166 | | - |
167 | | -### REST API wrapper (`@shared/services/rest-api/service.ts`) |
168 | | - |
169 | | -The REST API service: |
170 | | - |
171 | | -- uses Axios with a request interceptor that injects `Authorization: Bearer <token>` from persistent storage |
172 | | -- returns a consistent `ApiResponse<T>` shape: `{ status, data?, error? }` |
173 | | -- logs out automatically on `401` |
174 | | -- supports response caching into electron-store when `options.isCache` is enabled |
175 | | - |
176 | | -### Cache lookup helpers (`@shared/cache-responses.ts`) |
177 | | - |
178 | | -`cacheUser(userId)` fetches a cached user response by reconstructing the request URL key. |
179 | | - |
180 | | -## Shared UI Infrastructure |
181 | | - |
182 | | -- `@shared/menu/*`: defines the default application menu and allows `app.ts` to patch menu entries (e.g., dev tools). |
183 | | -- `@shared/tray/*`: defines the tray menu and tray icon setup; `app.ts` wires click handlers. |
184 | | -- `@shared/notification.ts`: initializes a reusable `Notification` instance. |
185 | | -- `@shared/logger.ts`: configures `electron-log` formatting and levels. |
186 | | -- `@shared/path-resolver.ts`: canonical paths to preload/UI/assets (use when you need to resolve locations reliably across dev/prod). |
187 | | - |
188 | | -## Feature Modules (How features are structured) |
189 | | - |
190 | | -Each feature typically follows this shape: |
191 | | - |
192 | | -- `ipc.ts`: exports `handleSend` and/or `handleInvoke` to integrate with the global IPC gateway |
193 | | -- `service.ts`: business logic and side effects (REST calls, filesystem, OS APIs) |
194 | | -- `window.ts`: window creation + show behavior (optional) |
195 | | -- `types.ts`: feature-specific types (optional) |
196 | | - |
197 | | -### app-version |
198 | | - |
199 | | -- `app-version/ipc.ts`: exposes `invoke` handler for retrieving the current app version. |
200 | | - |
201 | | -### app-preload |
202 | | - |
203 | | -- `app-preload/window.ts`: shows a frameless always-on-top spinner window (loads a local `spinner.html`). |
204 | | -- `app-preload/ipc.ts`: initializes the spinner on startup and listens for a `windowClosePreload` event to hide the spinner and show the main window. |
205 | | - |
206 | | -### auth |
207 | | - |
208 | | -- `auth/window.ts`: opens an auth/OAuth BrowserWindow pointing at the backend auth route. |
209 | | - - uses a dedicated persistent session partition (`persist:auth`) |
210 | | -- `auth/ipc.ts`: |
211 | | - - handles `logout` and `checkAuth` |
212 | | - - opens auth window when renderer requests `windowAuth` |
213 | | - - listens to `will-redirect` to detect: |
214 | | - - verify callback → extracts `token` and `userId` → persists them → notifies renderer (`receive` channel) |
215 | | - - user-exists error → shows dialog |
216 | | - |
217 | | -### user |
218 | | - |
219 | | -- `user/ipc.ts`: on `user` request, replies with cached user immediately (if present) and then fetches fresh user data. |
220 | | -- `user/service.ts`: fetches the user via the shared REST API wrapper and caches responses. |
221 | | - |
222 | | -### updater |
223 | | - |
224 | | -Updater has two implementations: |
225 | | - |
226 | | -- **Windows** (`updater/services/win/*`) uses `electron-updater`. |
227 | | - - `setFeedURL.ts` configures GitHub provider and optional token. |
228 | | - - `controlUpdater.ts` listens for updater events and pushes status to renderer (`sendUpdateInfo`). |
229 | | -- **macOS** (`updater/services/mac/*`) uses GitHub Releases API + manual download. |
230 | | - - checks latest version, compares versions, downloads `.dmg` to `~/Downloads/app-update/`, reports progress. |
231 | | - |
232 | | -Main entry points: |
233 | | - |
234 | | -- `updater/window.ts`: creates/shows the update window and triggers update checks. |
235 | | -- `updater/services/checkForUpdates.ts`: orchestrates platform-specific logic. |
236 | | -- `updater/ipc.ts`: handles renderer events (`checkForUpdates`, `restart`, `openLatestVersion`). |
237 | | - |
238 | | -### crash |
239 | | - |
240 | | -- `crash/service.ts`: sets process and app-level crash handlers: |
241 | | - - `uncaughtException` |
242 | | - - `unhandledRejection` |
243 | | - - `render-process-gone` |
244 | | - It tears down tray/preload UI and shows an error dialog. |
245 | | - |
246 | | -## How to Add a New Main-Process Feature Module |
247 | | - |
248 | | -1. Create `src/main/<feature>/`. |
249 | | -2. Add `ipc.ts` exporting `handleSend` and/or `handleInvoke`. |
250 | | -3. Put business logic in `service.ts` (use shared services where possible). |
251 | | -4. If a window is needed, create `window.ts` that calls `createWindow({ hash, isCache: true, options })`. |
252 | | -5. Add any new window hash to `src/main/config.ts -> windows`. |
253 | | -6. Wire the feature into `src/main/app.ts`: |
254 | | - - add imports |
255 | | - - include your handler in the `registerIpc({ onSend, onInvoke })` fan-out. |
256 | | -7. If you introduce new IPC message types, update the shared type declarations under the repository `types/` folder so `preload.cts` and renderer stay type-safe. |
257 | | - |
258 | | -## Testing Notes |
259 | | - |
260 | | -Main-process unit tests are co-located next to implementation files (e.g., `create.test.ts`). When writing new tests, mock Electron APIs (`ipcMain`, `BrowserWindow`, etc.) and focus on pure logic + IPC handler behavior. |
0 commit comments