Extend the pool warmup state machine to support singleton windows via an optional `singletonConfig` (eager pre-warm + retentionTime-based close→hide with delayed destroy). Generalize shared symbols (PoolState→WarmupState, lastOpenAt→lastActivityAt, pool*→warmup* on non-pool-specific methods) and fix a half-bug where pool inactivity only counted since last open, not since last open or close. Op naming convention: lifecycle-specific ops carry `pool-*` / `singleton-*` prefixes; ops shared by both (create-idle, release-skip, inactivity-trim, warmup) are unprefixed so log greps stay precise. Main is intentionally NOT migrated — its close handler reads tray preferences, quits the app on Win/Linux, guards on isFullScreen, and toggles Dock visibility; none of that fits the declarative `retentionTime` contract. Registry entry now documents this decision inline.
5.9 KiB
Window Migration Guide
How to migrate an existing window from direct BrowserWindow creation to WindowManager.
Step 1: Add the WindowType
In types.ts, add a new enum value:
export enum WindowType {
// ...
MyWindow = 'myWindow',
}
Step 2: Register in windowRegistry.ts
Define the window's metadata and default configuration:
WINDOW_TYPE_REGISTRY[WindowType.MyWindow] = {
type: WindowType.MyWindow,
lifecycle: 'singleton', // or 'default' or 'pooled'
htmlPath: 'my-window.html',
// preload omitted → defaults to 'index.js'. Write basename (with extension)
// to select a different file in src/preload/. Empty string → no preload.
// preload: 'simplest.js',
showMode: 'auto', // 'auto' | 'immediate' | 'manual'
windowOptions: {
...DEFAULT_WINDOW_CONFIG,
width: 800,
height: 600,
},
behavior: {
// Declarative WM-level behaviors (all optional). See the README "Configuration Layers" section.
// hideOnBlur: true, // auto-hide on blur (runtime override: wm.behavior.setHideOnBlur)
// alwaysOnTop: { level: 'floating' }, // level/relativeLevel for setAlwaysOnTop (runtime override: wm.behavior.setAlwaysOnTop)
// visibleOnAllWorkspaces: { enabled: true, visibleOnFullScreen: true },
// macShowInDock: false, // do not contribute to Dock visibility (macOS helper windows only; default true)
// // runtime override: wm.behavior.setMacShowInDockByType(type, value) for tray-mode transitions
},
// quirks: { ... }, // OS hacks — see Platform Configuration
}
See Lifecycle Modes for choosing between default / singleton / pooled.
Optional: for singleton types that benefit from pre-warm or close→hide, set singletonConfig. See Warmup Mechanics → Singleton Variant.
Step 3: Move domain logic to onWindowCreated
Replace direct new BrowserWindow() + setup code with an onWindowCreated subscription in your domain service:
Before:
class MyService {
private window: BrowserWindow | null = null
createWindow() {
this.window = new BrowserWindow({ width: 800, height: 600, ... })
this.window.loadFile('my-window.html')
this.window.on('closed', () => { this.window = null })
}
}
After:
@Injectable('MyService')
@ServicePhase(Phase.WhenReady)
class MyService extends BaseService {
private windowId: string | undefined
protected override onInit(): void {
const wm = application.get('WindowManager')
wm.onWindowCreatedByType(WindowType.MyWindow, ({ window, id }) => {
this.windowId = id
// attach listeners here — use `window` directly, or switch to the `mw` shorthand
// if the callback body has inner closures (see Usage Guide → Callback styles).
})
wm.onWindowDestroyedByType(WindowType.MyWindow, () => {
this.windowId = undefined
})
}
openWindow(): void {
const wm = application.get('WindowManager')
this.windowId = wm.open(WindowType.MyWindow)
}
}
See Injecting behavior: onWindowCreated is the canonical hook for the full rationale behind this pattern.
Step 4: Replace direct BrowserWindow references
| Old Pattern | New Pattern |
|---|---|
this.window = new BrowserWindow(...) |
wm.open(WindowType.MyWindow) |
this.window.show() |
wm.show(windowId) |
this.window.hide() |
wm.hide(windowId) |
this.window.close() |
wm.close(windowId) |
this.window.webContents.send(...) |
wm.getWindow(windowId)?.webContents.send(...) or wm.broadcastToType(...) |
BrowserWindow.fromWebContents(e.sender) |
wm.getWindowIdByWebContents(e.sender) |
Note: there is intentionally no entry for this.window.destroy(). wm.close() already handles destruction for non-pooled windows and pool-return for pooled windows. wm.destroy() is an internal primitive — see Window API layers.
Step 5: Handle show behavior
Remove manual show / ready-to-show logic if using showMode: 'auto' (the default). WindowManager handles:
- Creating the window hidden
- Showing on
ready-to-show(fresh path) or immediately (recycled path)
If your window needs custom show timing, set showMode: 'manual' in the registry and manage visibility yourself.
Checklist
- Added
WindowTypeenum value intypes.ts - Registered metadata in
WINDOW_TYPE_REGISTRYinwindowRegistry.ts - Chose the correct lifecycle mode (
default/singleton/pooled) - Set
preloadfilename if not using the default ('index.js') - Set
showModebehavior ('auto'/'immediate'/'manual') - Set
behavior.macShowInDock: falseONLY for helper windows (floating panels, selection overlays); primary app windows leave it at the defaulttrue. Usewm.behavior.setMacShowInDockByType(type, value)for runtime tray-mode transitions, not a different registry default. - Declared
behavior.hideOnBlur/behavior.alwaysOnTop/behavior.visibleOnAllWorkspacesas needed - Moved domain logic from constructor to
onWindowCreatedhook - Replaced direct
BrowserWindowreferences with WindowManager API calls - Removed manual
ready-to-showhandling (if usingshowMode: 'auto') - If the window consumes init data: replaced hand-rolled
getInitData+ reset IPC wiring with theuseWindowInitDatahook - If pooled: chose appropriate
PoolConfigaxes (standbySizefor active pre-warm,recycleMinSize/recycleMaxSizefor recycling). LeaverecycleMaxSizeunset for one-shot "close destroys" semantics; setstandbySizewhen zero-wait matters under concurrent opens. - Verified
onWindowDestroyedcleanup in the domain service