Skip to content
Merged

Lazy #15

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@
- Code of Conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
- Security policy: [SECURITY.md](SECURITY.md)

## Navigation

- [Overview](#overview) — High-level summary and goals.
- [What It Solves](#what-it-solves) — Practical pain points addressed.
- [What You Get](#what-you-get) — Core capabilities provided.
- [Key Features](#key-features) — Quick feature checklist.
- [Example App](#example-app) — Starter/example repositories.
- [Installation](#installation) — Install and peer dependency notes.
- [Folders (build outputs)](#folders-build-outputs) — Build output locations.
- [Quick Start](#quick-start) — Minimal bootstrap example for `main.ts`.
- [Module Structure](#module-structure) — Recommended file layout for a feature module.
- [Two Approaches to Using Modules](#two-approaches-to-using-modules) — Design choices for module APIs.
- [Approach 1: Direct Service Injection](#approach-1-direct-service-injection-simple) — Simple DI usage.
- [Approach 2: Provider Pattern](#approach-2-provider-pattern-advanced) — Provider/factory-based contracts.
- [IPC Handlers](#ipc-handlers) — Organizing main ↔ renderer communication.
- [Window Managers](#window-managers) — Window lifecycle and helpers.
- [Preload script — default behavior](#preload-script--default-behavior-) — Default preload handling.
- [Lifecycle Hooks (Window & WebContents events)](#lifecycle-hooks-window--webcontents-events-) — Event hook mapping.
- [Opening windows with URL params](#opening-windows-with-url-params-dynamic-routes-) — Route/hash usage.
- [TypeScript types — TWindows["myWindow"]](#typescript-types--twindowsmywindow) — Typing conventions for windows.
- [API Reference](#api-reference) — Reference for decorators and core functions.
- [Core Decorators](#core-decorators) — `@RgModule`, `@Injectable`, `@Inject`, `@IpcHandler`, `@WindowManager`.
- [Core Functions](#core-functions) — `initSettings`, `bootstrapModules`, `getWindow`, `destroyWindows`.
- [Lazy Loading modules](#lazy-loading-modules) — Deferred init behavior and constraints.
- [Lifecycle Interfaces](#lifecycle-interfaces) — IPC and window interface contracts.
- [Best Practices](#best-practices) — Recommended development patterns.
- [Type Everything](#5-type-everything) — Type-safety guidance and tips.

## Overview

A lightweight dependency injection container for Electron's main process that brings modular architecture and clean code organization to your desktop applications.
Expand Down Expand Up @@ -544,6 +572,7 @@ Defines a module with its dependencies and providers.
- `ipc?: Class[]` - IPC handler classes
- `windows?: Class[]` - Window manager classes
- `exports?: Class[]` - Providers to export
- `lazy?: { enabled: true; trigger: string }` - Defers module initialization until renderer invokes the trigger channel

#### `@Injectable()`

Expand Down Expand Up @@ -643,6 +672,110 @@ Bootstraps all modules and initializes the DI container.
await bootstrapModules([AppModule, AuthModule, ResourcesModule]);
```

Lazy modules are registered but not initialized during bootstrap. Initialization happens on first `ipcRenderer.invoke(trigger)` from renderer process.

```typescript
@RgModule({
providers: [AnalyticsService],
ipc: [AnalyticsIpc],
lazy: {
enabled: true,
trigger: "analytics",
},
})
export class AnalyticsModule {}

await bootstrapModules([UserModule, AnalyticsModule]);
// UserModule: initialized immediately
// AnalyticsModule: initialized on first ipcRenderer.invoke("analytics")
```

Notes:

- Lazy loading defers runtime initialization work (provider resolution, module instantiation, IPC `onInit`).
- It does not perform JavaScript code-splitting by itself; module code is still loaded by your app bundle strategy.
- Each lazy trigger must be unique across modules in the same bootstrap call.

### Lazy Loading modules

Lazy Loading lets you defer module initialization until the renderer explicitly requests it via `ipcRenderer.invoke(trigger)`.

Why it exists:

- Reduces startup work in the main process for modules that are not needed immediately.
- Improves perceived startup time when some features are rarely used.

When to use it:

- Heavy modules (database connections, expensive service wiring, feature-specific IPC setup).
- Feature modules opened only after a user action (analytics, advanced settings, reports).

When it usually does not help:

- Modules required at application start (auth bootstrap, app shell wiring, core windows).
- Very small modules where deferred initialization adds complexity without measurable gain.

Important constraints:

- A lazy module **cannot** declare `exports`.
- A lazy module can import only eager modules.
- An eager module cannot import a lazy module.

These constraints guarantee clear module boundaries: lazy modules are activated explicitly, while shared cross-module dependencies stay eager and deterministic.

#### Example: valid lazy module

```typescript
@RgModule({
imports: [DatabaseCoreModule], // eager module
providers: [AnalyticsService],
ipc: [AnalyticsIpc],
lazy: {
enabled: true,
trigger: "analytics:init",
},
})
export class AnalyticsModule {}

await bootstrapModules([AppModule, AnalyticsModule]);

// Renderer side:
await ipcRenderer.invoke("analytics:init");
```

#### Example: invalid (lazy + exports)

```typescript
@RgModule({
providers: [AnalyticsService],
exports: [AnalyticsService], // ❌ not allowed for lazy modules
lazy: {
enabled: true,
trigger: "analytics:init",
},
})
export class AnalyticsModule {}
```

#### Example: invalid (eager imports lazy)

```typescript
@RgModule({
providers: [],
lazy: {
enabled: true,
trigger: "database:init",
},
})
export class DatabaseModule {}

@RgModule({
imports: [DatabaseModule], // ❌ eager module cannot import lazy module
providers: [ReportsService],
})
export class ReportsModule {}
```

#### `getWindow<T>(hash)`

Retrieves a window instance by its hash identifier.
Expand Down Expand Up @@ -784,6 +917,7 @@ Defines a module.
- `ipc?: Class[]` - IPC handler classes
- `windows?: Class[]` - Window manager classes
- `exports?: Class[]` - Providers to export
- `lazy?: { enabled: true; trigger: string }` - Defers module initialization until renderer invokes the trigger channel

### `@Injectable()`

Expand Down
40 changes: 21 additions & 19 deletions coverage/coverage-final.json

Large diffs are not rendered by default.

40 changes: 20 additions & 20 deletions coverage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,30 @@ <h1>All files</h1>
<div class='clearfix'>

<div class='fl pad1y space-right2'>
<span class="strong">98.47% </span>
<span class="strong">97.89% </span>
<span class="quiet">Statements</span>
<span class='fraction'>452/459</span>
<span class='fraction'>558/570</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">96.35% </span>
<span class="strong">94.27% </span>
<span class="quiet">Branches</span>
<span class='fraction'>132/137</span>
<span class='fraction'>181/192</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>36/36</span>
<span class='fraction'>40/40</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">98.47% </span>
<span class="strong">97.89% </span>
<span class="quiet">Lines</span>
<span class='fraction'>452/459</span>
<span class='fraction'>558/570</span>
</div>


Expand Down Expand Up @@ -100,8 +100,8 @@ <h1>All files</h1>
</td>
<td data-value="99.56" class="pct high">99.56%</td>
<td data-value="231" class="abs high">230/231</td>
<td data-value="98.41" class="pct high">98.41%</td>
<td data-value="63" class="abs high">62/63</td>
<td data-value="98.43" class="pct high">98.43%</td>
<td data-value="64" class="abs high">63/64</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="17" class="abs high">17/17</td>
<td data-value="99.56" class="pct high">99.56%</td>
Expand All @@ -110,17 +110,17 @@ <h1>All files</h1>

<tr>
<td class="file high" data-value="src/@core/bootstrap"><a href="src/@core/bootstrap/index.html">src/@core/bootstrap</a></td>
<td data-value="95.71" class="pic high">
<td data-value="95.61" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 95%"></div><div class="cover-empty" style="width: 5%"></div></div>
</td>
<td data-value="95.71" class="pct high">95.71%</td>
<td data-value="140" class="abs high">134/140</td>
<td data-value="90.9" class="pct high">90.9%</td>
<td data-value="44" class="abs high">40/44</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="10" class="abs high">10/10</td>
<td data-value="95.71" class="pct high">95.71%</td>
<td data-value="140" class="abs high">134/140</td>
<td data-value="95.61" class="pct high">95.61%</td>
<td data-value="251" class="abs high">240/251</td>
<td data-value="89.69" class="pct high">89.69%</td>
<td data-value="97" class="abs high">87/97</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="14" class="abs high">14/14</td>
<td data-value="95.61" class="pct high">95.61%</td>
<td data-value="251" class="abs high">240/251</td>
</tr>

<tr>
Expand Down Expand Up @@ -161,7 +161,7 @@ <h1>All files</h1>
<td data-value="100" class="pct high">100%</td>
<td data-value="22" class="abs high">22/22</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="9" class="abs high">9/9</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="1" class="abs high">1/1</td>
<td data-value="100" class="pct high">100%</td>
Expand All @@ -176,7 +176,7 @@ <h1>All files</h1>
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-01-24T11:37:51.253Z
at 2026-02-14T22:18:43.763Z
</div>
<script src="prettify.js"></script>
<script>
Expand Down
40 changes: 20 additions & 20 deletions coverage/lcov-report/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,30 @@ <h1>All files</h1>
<div class='clearfix'>

<div class='fl pad1y space-right2'>
<span class="strong">98.47% </span>
<span class="strong">97.89% </span>
<span class="quiet">Statements</span>
<span class='fraction'>452/459</span>
<span class='fraction'>558/570</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">96.35% </span>
<span class="strong">94.27% </span>
<span class="quiet">Branches</span>
<span class='fraction'>132/137</span>
<span class='fraction'>181/192</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>36/36</span>
<span class='fraction'>40/40</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">98.47% </span>
<span class="strong">97.89% </span>
<span class="quiet">Lines</span>
<span class='fraction'>452/459</span>
<span class='fraction'>558/570</span>
</div>


Expand Down Expand Up @@ -100,8 +100,8 @@ <h1>All files</h1>
</td>
<td data-value="99.56" class="pct high">99.56%</td>
<td data-value="231" class="abs high">230/231</td>
<td data-value="98.41" class="pct high">98.41%</td>
<td data-value="63" class="abs high">62/63</td>
<td data-value="98.43" class="pct high">98.43%</td>
<td data-value="64" class="abs high">63/64</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="17" class="abs high">17/17</td>
<td data-value="99.56" class="pct high">99.56%</td>
Expand All @@ -110,17 +110,17 @@ <h1>All files</h1>

<tr>
<td class="file high" data-value="src/@core/bootstrap"><a href="src/@core/bootstrap/index.html">src/@core/bootstrap</a></td>
<td data-value="95.71" class="pic high">
<td data-value="95.61" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 95%"></div><div class="cover-empty" style="width: 5%"></div></div>
</td>
<td data-value="95.71" class="pct high">95.71%</td>
<td data-value="140" class="abs high">134/140</td>
<td data-value="90.9" class="pct high">90.9%</td>
<td data-value="44" class="abs high">40/44</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="10" class="abs high">10/10</td>
<td data-value="95.71" class="pct high">95.71%</td>
<td data-value="140" class="abs high">134/140</td>
<td data-value="95.61" class="pct high">95.61%</td>
<td data-value="251" class="abs high">240/251</td>
<td data-value="89.69" class="pct high">89.69%</td>
<td data-value="97" class="abs high">87/97</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="14" class="abs high">14/14</td>
<td data-value="95.61" class="pct high">95.61%</td>
<td data-value="251" class="abs high">240/251</td>
</tr>

<tr>
Expand Down Expand Up @@ -161,7 +161,7 @@ <h1>All files</h1>
<td data-value="100" class="pct high">100%</td>
<td data-value="22" class="abs high">22/22</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="9" class="abs high">9/9</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="1" class="abs high">1/1</td>
<td data-value="100" class="pct high">100%</td>
Expand All @@ -176,7 +176,7 @@ <h1>All files</h1>
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-01-24T11:37:51.308Z
at 2026-02-14T22:18:43.807Z
</div>
<script src="prettify.js"></script>
<script>
Expand Down
Loading