├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src ├── lib │ ├── main_lib.ts │ ├── restorer_lib.ts │ ├── shared_lib.ts │ └── window_lib.ts ├── main.ts ├── preload.ts └── window.ts ├── test ├── api │ ├── bad_mainapi.ts │ ├── bad_windowapi.ts │ ├── call_mainapi1.ts │ ├── call_mainapi2.ts │ ├── call_mainapi3a.ts │ ├── call_mainapi3b.ts │ ├── call_mainapi4.ts │ ├── call_winapi1.ts │ ├── call_winapi2.ts │ ├── call_winapi3.ts │ ├── mainapi.ts │ ├── mainapi1.ts │ ├── mainapi2.ts │ ├── mainapi3.ts │ ├── mainapi4.ts │ ├── verify_mainapi1.ts │ ├── verify_mainapi2.ts │ ├── verify_mainapi4.ts │ ├── verify_winapi1.ts │ ├── verify_winapi2.ts │ ├── verify_winapi3.ts │ ├── winapi1.ts │ ├── winapi2.ts │ └── winapi3.ts ├── lib │ ├── config.ts │ ├── main_util.ts │ ├── renderer_util.ts │ └── shared_util.ts ├── main-scripts │ ├── main_nonrelayed_error.ts │ ├── main_return_relayed.ts │ ├── main_win1_timeout1.ts │ ├── main_win1_timeout2.ts │ ├── main_win2_timeout.ts │ └── main_window_closed.ts ├── run-tests ├── test_calling_windows.ts ├── test_main_crash.ts ├── test_main_patience.ts ├── test_single_main_api.ts ├── test_single_window_api.ts ├── test_type_errors.ts ├── test_window_reload.ts ├── test_window_timeout.ts ├── test_windows_calling.ts └── window-scripts │ ├── dummy.html │ ├── tsconfig.json │ ├── win1_mainapi1+2.ts │ ├── win1_mainapi1.ts │ ├── win1_mainapi2_reload_1.ts │ ├── win1_mainapi2_reload_2.ts │ ├── win1_mainapi3a.ts │ ├── win1_mainapi3b.ts │ ├── win1_winapi1+2.ts │ ├── win1_winapi1.ts │ ├── win1_winapi1_delayed.ts │ ├── win2_mainapi1+4.ts │ ├── win2_mainapi2_timeout.ts │ ├── win2_winapi1+3.ts │ └── win3_mainapi2_timeout.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joseph T. Lapp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-affinity 2 | 3 | Electron IPC via simple method calls 4 | 5 | ## Introduction 6 | 7 | Electron Affinity is a small TypeScript library that makes IPC as simple as possible in Electron. It has the following features: 8 | 9 | - IPC services are merely methods on vanilla classes, callable both locally and remotely. 10 | - Organizes IPC methods into distinct named APIs, each defined by its own class. 11 | - Makes APIs remotely available merely by handing instances of their classes to a library function. 12 | - Remotely binds APIs merely by passing their names to a library function. 13 | - Changes made to the TypeScript signature of an API instantly change the remotely available signature. 14 | - Optionally restores transferred objects back into classes via custom restoration functions, enabling APIs to have class instance parameters and return values. 15 | - Allows main APIs to cause exceptions to be thrown in the calling window by wrapping the exception in an instance of `RelayedError` and throwing this instance. 16 | - Main APIs are all asynchronous functions using Electron's `ipcRenderer.invoke`, while window APIs all use Electron's `window.webContents.send` and return no value. 17 | - Uses context isolation and does not require node integration, maximizing security. 18 | 19 | In short, this library implements IPC as remote procedure calls (RPC). For an overview of the library's inner workings, you might read [this article](https://javascript.plainenglish.io/electron-rpc-using-the-magic-of-typescript-9d24530ea8f1). 20 | 21 | > NOTE ON JAVASCRIPT: The library should work with plain JavaScript, but I have not tried it, so I don't know what special considerations might require documentation. 22 | 23 | ## Problems Addressed 24 | 25 | This library was designed to address many of the problems that can arise when using IPC in Electron. Every design decision was intended to either eliminate a problem or catch a problem and produce a helpful error message. Here are some of the problems addressed: 26 | 27 | - Misspelled or inconsistently changed IPC channel names break calls. 28 | - There are two channel name spaces, and an IPC can be handled in one but called in the other. 29 | - Types for each IPC are managed in multiple places, allowing argument and return types to disagree between the main process and the renderer. 30 | - Class instances become untyped objects when transmitted over IPC. 31 | - Implementing IPC requires lots of boilerplate code on both sides. 32 | - Extra effort is required to make local IPC functionality locally available. 33 | - Exceptions are local, with no mechanism for transferring caller-caused errors back to the caller to be rethrown. 34 | - Coding IPC with context isolation, without node integration is typically complex. 35 | 36 | _Special thanks to **[Electron Mocha](https://github.com/jprichardson/electron-mocha)**, which made it possible for me to thoroughly test each iteration of the solution until everything worked as expected._ 37 | 38 | ## Installation 39 | 40 | `npm install electron-affinity` 41 | 42 | or 43 | 44 | `yarn add electron-affinity` 45 | 46 | ## Usage 47 | 48 | Electron Affinity supports main APIs and window APIs. Main APIs are defined in the main process and callable from renderer windows. Window APIs are defined in renderer windows and callable from the main process. Window-to-window calling is not supported, as [JavaScript allows for messaging between windows](https://stackoverflow.com/a/68868073/650894), and it is an option to relay all communication through the main process. 49 | 50 | The first two sections on usage, "Main APIs" and "Window APIs", are all you need to read to get an understanding of this library. 51 | 52 | ### Main APIs 53 | 54 | A main API is an instance of a class defined in the main process. All methods of this class, including ancestor class methods, are treated as IPC methods except for those prefixed with underscore (`_`) or pound (`#`). You can therefore use these prefixes to define private properties and methods on which the IPC methods rely. 55 | 56 | Each main API method can take any number of parameters, including none, but must return a promise. The promise need not resolve to a value. 57 | 58 | Here is an example main API called `DataApi`: 59 | 60 | ```ts 61 | import { ElectronMainApi, RelayedError } from "electron-affinity/main"; 62 | 63 | export class DataApi implements ElectronMainApi { 64 | _dataSource: DataSource; 65 | _dataset: Dataset | null = null; 66 | 67 | constructor(dataSource: DataSource) { 68 | this._dataSource = dataSource; 69 | } 70 | 71 | async openDataset(setName: string, timeout: number) { 72 | this._dataset = this._dataSource.open(setName, timeout); 73 | } 74 | 75 | async readData() { 76 | const data = await this._dataset!.read(); 77 | this._checkForError(); 78 | return data; 79 | } 80 | 81 | async writeData(data: Data) { 82 | await this._dataset!.write(data.format()); 83 | this._checkForError(); 84 | } 85 | 86 | async closeDataset() { 87 | this._dataset!.close(); 88 | this._dataset = null; 89 | } 90 | 91 | private _checkForError() { // okay to use 'private' with '_' 92 | const err: DataError | null = this._dataSource.getError(); 93 | if (err) throw new RelayedError(err); 94 | } 95 | } 96 | ``` 97 | 98 | Here are a few things to note about this API: 99 | 100 | - The API implements `ElectronMainApi` to get enforcement of main API type constraints, replacing `T` with the API's own class name. 101 | - All methods return promises even when they don't need to. This allows all IPC calls to the main process to use `ipcRenderer.invoke()`, keeping Electron Affinity simple. 102 | - Even though `writeData()` received `data` via IPC, it exists as an instance of class `Data` with the `format()` method available. 103 | - Property names `_dataSource`, `_dataset`, and `_checkforError()` are not exposed as IPC methods because they prefixed with `_`. In ES2022 you can also use the `#` prefix. 104 | - It's okay to also use `private` and `protected` modifiers. These modifiers do cause TypeScript to error if you attempt to remotely access the properties they modify, but remote bindings will still be created for any property not prefixed with `_` or `#`. 105 | - If the data source encounters an error, `_checkForError()` returns the error (sans stack trace) to the window to be thrown from within the renderer. 106 | - Exceptions thrown by `open()`, `read()`, or `write()` do not get returned to the window and instead cause exceptions within the main process. 107 | 108 | The main process makes this API available to windows by calling `exposeMainApi` before the windows attempt to use the API: 109 | 110 | ```ts 111 | import { exposeMainApi } from "electron-affinity/main"; 112 | 113 | exposeMainApi(new DataApi(dataSource), restorer); 114 | ``` 115 | 116 | `restorer` is an optional function-typed argument that takes responsibility for restoring the classes of transferred objects. It only restores those classes that the API requires be restored. [See below](#restoring-classes) for an explanation of its use. 117 | 118 | A window uses the API as follows: 119 | 120 | ```ts 121 | import { bindMainApi } from "electron-affinity/window"; 122 | import type { DataApi } from "path/to/data_api"; 123 | 124 | async function loadWeatherData() { 125 | const dataApi = await bindMainApi("DataApi"); 126 | 127 | dataApi.openDataset("weather-data", 500); 128 | try { 129 | let data = await dataApi.readData(); 130 | while (data !== null) { 131 | /* do something with the data */ 132 | data = await dataApi.readData(); 133 | } 134 | } catch (err) { 135 | if (err instanceof DataError) { 136 | /* handle relayed error */ 137 | } 138 | } 139 | dataApi.closeDataset(); 140 | } 141 | ``` 142 | 143 | Note the following about calling the API: 144 | 145 | - The code imports the _type_ `DataApi` rather than the class `DataApi`. This keeps the renderer from pulling in main process code. 146 | - `bindMainApi()` takes both the type parameter `DataApi` and the string name of the API `"DataApi"`. The names must agree. If they don't, you'll get a runtime error saying that the API cannot be found. 147 | - The main process must have previously exposed the API. The window will not wait for the main process to subsequently expose it. 148 | - The code calls a main API method as if it were local to the window. 149 | - There is no need to wait on APIs, particularly those that technically didn't need to be declared asynchronous (but were to satisfy Electron Affinity). 150 | 151 | Finally, include the following line in your `preload.js`: 152 | 153 | ```ts 154 | import "electron-affinity/preload"; 155 | ``` 156 | 157 | Alternatively, preload directly from `node_modules` using the appropriate path: 158 | 159 | ```ts 160 | // src/main.ts 161 | 162 | const window = new BrowserWindow({ 163 | webPreferences: { 164 | preload: path.join( 165 | __dirname, 166 | "../node_modules/electron-affinity/preload.js" 167 | ), 168 | nodeIntegration: false, 169 | contextIsolation: true, 170 | }, 171 | }); 172 | ``` 173 | 174 | ### Window APIs 175 | 176 | Window APIs are analogous to main APIs, except that they are defined in the renderer and don't return a value. All methods of a window API class, including ancestor class methods, are treated as IPC methods except for those prefixed with underscore (`_`) or pound (`#`). As with main APIs, they can take any number of parameters, including none. 177 | 178 | Here is an example window API called `StatusApi`: 179 | 180 | ```ts 181 | import type { ElectronWindowApi } from 'electron-affinity/window'; 182 | 183 | export class StatusApi implements ElectronWindowApi { 184 | _receiver: StatusReceiver; 185 | 186 | constructor(receiver: StatusReceiver) { 187 | this._receiver = receiver; 188 | } 189 | 190 | progressUpdate(percentCompleted: number) { 191 | this._receiver.updateStatusBar(percentCompleted); 192 | } 193 | 194 | async systemReport(report: SystemReport) { 195 | const summary = await report.generateSummary(); 196 | this._receiver.updateMessage(summary); 197 | } 198 | } 199 | ``` 200 | 201 | Note the following about this API: 202 | 203 | - The API implements `ElectronWindowApi` to get enforcement of window API type constraints, replacing `T` with the API's own class name. 204 | - The methods are implemented as `window.webContents.send()` calls; the return values of window API methods are not returned. Code within the main process always shows their return values as void. 205 | - Methods can by asynchronous, but the main process cannot wait for them to resolve. 206 | - Even though `systemReport` received `report` via IPC, it exists as an instance of `SystemReport` with the `generateSummary()` method available. 207 | - The `_` prefix prevents the member `_receiver` from being exposed as an IPC method. The prefix `#` would have done the same in ES2022. Declaring a property `private` or `protected` is sufficient to keep TypeScript from allowing you to remotely reference a property, but it is insufficient to keep remote bindings from being generated for the property. 208 | - Exceptions thrown by any of these methods do not get returned to the main process. 209 | 210 | The window makes the API available to the main process by calling `exposeWindowApi`: 211 | 212 | ```ts 213 | import { exposeWindowApi } from "electron-affinity/window"; 214 | 215 | exposeWindowApi(new StatusApi(receiver), restorer); 216 | ``` 217 | 218 | `restorer` is an optional function-typed argument that takes responsibility for restoring the classes of transferred objects. It only restores those classes that the API requires be restored. [See below](#restoring-classes) for an explanation of its use. 219 | 220 | The main process uses the API as follows: 221 | 222 | ```ts 223 | import { bindWindowApi } from "electron-affinity/main"; 224 | import type { StatusApi } from "path/to/status_api"; 225 | 226 | async function doWork() { 227 | const statusApi = await bindWindowApi(window, "StatusApi"); 228 | 229 | /* ... */ 230 | statusApi.progressUpdate(percentCompleted); 231 | /* ... */ 232 | statusApi.systemReport(report); 233 | /* ... */ 234 | } 235 | ``` 236 | 237 | Note the following about calling the API: 238 | 239 | - The code imports the _type_ `StatusApi` rather than the class `StatusApi`. This keeps the main process from pulling in window-side code. 240 | - `bindWindowApi()` takes both the type parameter `StatusApi` and the string name of the API `"StatusApi"`. The names must agree. If they don't, you'll get a runtime error saying that the API cannot be found. 241 | - `bindWindowApi()` takes a reference to the `BrowserWindow` to which the API is bound. Each API is bound to a single window and messages only that window. 242 | - The code calls a window API method as if it were local to the main process. 243 | - The main process does not need to do anything special to wait for the window to finish loading. `bindWindowApi` will keep attempting to bind until timing out. 244 | 245 | Window APIs also require that you preload `electron-affinity/preload`. 246 | 247 | ### Organizing Main APIs 248 | 249 | Each main API must be exposed and bound individually. A good practice is to define each API in its own file, exporting the API class. Your main process code then imports them and exposes them one at a time. For example: 250 | 251 | ```ts 252 | // src/main.ts 253 | 254 | import { exposeMainApi } from "electron-affinity/main"; 255 | import type { DataApi } from "path/to/status_api"; 256 | import type { UploadApi } from "path/to/message_api"; 257 | 258 | exposeMainApi(new DataApi()); 259 | exposeMainApi(new UploadApi()); 260 | ``` 261 | 262 | However, the main process may want to call these APIs itself. In this case, it's useful to attach them to the `global` variable. We might do so as follows: 263 | 264 | ```ts 265 | // src/backend/apis/main_apis.ts 266 | 267 | import { exposeMainApi } from "electron-affinity/main"; 268 | import { DataApi } from "path/to/status_api"; 269 | import { UploadApi } from "path/to/message_api"; 270 | 271 | export type MainApis = ReturnType; 272 | 273 | export function installMainApis() { 274 | const apis = { 275 | dataApi: new DataApi(), 276 | uploadApi: new UploadApi(), 277 | /* ... */ 278 | }; 279 | for (const api of Object.values(apis)) { 280 | exposeMainApi(api as any); 281 | } 282 | global.mainApis = apis as any; 283 | } 284 | ``` 285 | 286 | (See [Generic Use of APIs](#generic-use-of-apis) for how to type check APIs in the above code without relying on the API classes having implemented `ElectronMainApi` or `ElectronWindowApi`.) 287 | 288 | This approach doesn't give us type-checking on calls to the APIs made from within the main process. To get this, put the following in a `global.d.ts` file: 289 | 290 | ```ts 291 | // src/backend/global.d.ts 292 | 293 | import { MainApis } from "./backend/apis/main_apis"; 294 | 295 | declare global { 296 | var mainApis: MainApis; 297 | } 298 | ``` 299 | 300 | Finally, call `installMainApis` when initializing the main process. Now any main process code can call the APIs: 301 | 302 | ```ts 303 | global.mainApis.dataApi.openDataset("weather-data", 500); 304 | let data = await global.mainApis.dataApi.readData(); 305 | await global.mainApis.uploadApi.upload(filename); 306 | ``` 307 | 308 | Windows are able to bind to main APIs after the main process has installed them, but a window must wait for each binding to complete before using the API. This requires API bindings to occur within asynchronous functions. One way to do this is to create a function for just this purpose: 309 | 310 | ```ts 311 | // src/frontend/main_apis.ts 312 | 313 | import { bindMainApi, AwaitedType } from "electron-affinity/window"; 314 | 315 | import type { DataApi } from "path/to/status_api"; 316 | import type { UploadApi } from "path/to/message_api"; 317 | 318 | export type MainApis = AwaitedType; 319 | 320 | export async function bindMainApis() { 321 | return { 322 | dataApi: await bindMainApi("DataApi"), 323 | uploadApi: await bindMainApi("UploadApi"), 324 | /* ... */ 325 | }; 326 | } 327 | ``` 328 | 329 | During initialization, have the window script call `bindMainApis`: 330 | 331 | ```ts 332 | // src/frontend/init.ts 333 | 334 | window.apis = await bindMainApis(); 335 | ``` 336 | 337 | To get type-checking on calls to these APIs, add the following to `global.d.ts`: 338 | 339 | ```ts 340 | // src/frontend/global.d.ts 341 | 342 | import type { MainApis } from "./lib/main_apis"; 343 | 344 | declare global { 345 | interface Window { 346 | apis: MainApis; 347 | } 348 | } 349 | ``` 350 | 351 | Assuming all windows bind to all main APIs, you can use `window.apis` to call any of the main APIs: 352 | 353 | ```ts 354 | window.apis.dataApi.openDataset("weather-data", 500); 355 | let data = await window.apis.dataApi.readData(); 356 | await window.apis.uploadApi.upload(filename); 357 | ``` 358 | 359 | ### Organizing Window APIs 360 | 361 | Each window API must be exposed and bound individually. A good practice is to define each API in its own file, exporting the API class. Your window script then imports them and exposes them one at a time. For example: 362 | 363 | ```ts 364 | // src/frontend/init.ts 365 | 366 | import { exposeWindowApi } from "electron-affinity/window"; 367 | 368 | import { StatusApi } from "./apis/status_api"; 369 | import { MessageApi } from "./apis/message_api"; 370 | import { ReportStatusApi } from "./apis/report_status_api"; 371 | 372 | exposeWindowApi(new StatusApi()); 373 | exposeWindowApi(new MessageApi()); 374 | exposeWindowApi(new ReportStatusApi()); 375 | /* ... */ 376 | ``` 377 | 378 | It helps to create a module in the main process that binds the APIs for each different kind of window. In the following, `AwaitedType` extracts the type of value to which a promise resolves and prevents you from having to redeclare the API: 379 | 380 | ```ts 381 | // src/backend/window_apis.ts 382 | 383 | import type { BrowserWindow } from "electron"; 384 | import { AwaitedType, bindWindowApi } from "electron-affinity/main"; 385 | 386 | import type { StatusApi } from "../frontend/apis/status_api"; 387 | import type { MessageApi } from "../frontend/apis/message_api"; 388 | 389 | export type MainWindow = AwaitedType; 390 | export type ReportWindow = AwaitedType; 391 | 392 | export async function bindMainWindowApis(window: BrowserWindow) { 393 | return Object.assign(window, { 394 | apis: { 395 | statusApi: await bindWindowApi(window, "StatusApi"), 396 | messageApi: await bindWindowApi(window, "MessageApi"), 397 | }, 398 | }); 399 | } 400 | 401 | export async function bindReportWindowApis(window: BrowserWindow) { 402 | return Object.assign(window, { 403 | apis: { 404 | reportStatusApi: await bindWindowApi( 405 | window, 406 | "ReportStatusApi" 407 | ), 408 | }, 409 | }); 410 | } 411 | ``` 412 | 413 | These bind methods place APIs on `window.apis`. Here is how you might attach `apis` to the main window: 414 | 415 | ```ts 416 | // src/main.ts 417 | 418 | import { MainWindow } from "./window_apis"; 419 | 420 | function createMainWindow(): MainWindow { 421 | const mainWindow = new BrowserWindow({ 422 | /* ... */ 423 | }) as MainWindow; 424 | mainWindow.loadURL(url).then(async () => { 425 | await bindMainWindowApis(mainWindow); 426 | /* ... */ 427 | }); 428 | /* ... */ 429 | return mainWindow; 430 | } 431 | ``` 432 | 433 | Notice that (1) the window must exist in order to bind to any of its APIs, and (2) if you're going to wait for the binding to complete, you must have previously loaded the window script that exposes the APIs to be bound. 434 | 435 | And now you can call the APIs as follows: 436 | 437 | ```ts 438 | mainWindow.apis.statusApi.progressUpdate(progressPercent); 439 | mainWindow.apis.messageApi.sendMessage(message); 440 | ``` 441 | 442 | > NOTE FOR SVELTE: If your window API needs to import from svelte modules, you'll want to put the API within `