import type { Disposable, RuntimeBackend, Runtime, SenderPolicy, DeckConfig } from '../types.js'; import type { MinimalBrowserWindow, MinimalElectron, MinimalWebContentsView } from './electron-types.js'; import type { LifecyclePhase } from './lifecycle-manager.js'; import { type Scope } from '../main/scope.js'; import { type Compositor } from '../main/compositor.js'; import { type CapabilityPolicy } from '../host/capability.js'; import { type MinimalIpcMain, type MinimalWebContents } from './wire-transport.js'; /** * Optional dependencies for {@link DeckApp} —— 用于注入真 (或 * mock) Electron `ipcMain` + trusted webContents 集合,让 framework 接通跨进程 * wire transport。不注入则保持 main-internal-only 行为。 * * @internal */ export interface DeckAppOptions { readonly wireTransport?: { readonly ipcMain: MinimalIpcMain; /** 默认返回 framework 内部维护的 trusted set(由 windows.trust 填)。 */ readonly trustedWebContents?: () => readonly MinimalWebContents[]; /** 自定义 senderPolicy(默认按 trusted set 判断)。 */ readonly senderPolicy?: SenderPolicy; }; /** * 注入真 (或 fake) Electron `BrowserWindow` / `WebContentsView` * 构造器;提供后 framework 会装配 mainWindow / toolbarView / declared * windows,否则保持 electron-unavailable 行为。 */ readonly electron?: MinimalElectron; /** * 领域 backend。提供后 framework 在 whenReady 前跑 `beforeReady`, * 在 setup 阶段跑 `assemble(runtime)`。不提供则退化为纯框架(桩 context, * 仅测试/演示用)。 */ readonly backend?: RuntimeBackend; } /** * Per-window native-view substrate. Each tracked window * gets ONE `Compositor` whose {@link ContentViewHost} adapts that window's * `contentView`. The minimal `contentView` has no `children()`, so the host * adapter TRACKS the Compositor-managed views' z-order itself (the toolbar, * added directly to `contentView`, is invisible to this `order`). `registerView` * binds a view id to its native `WebContentsView` so the adapter can translate a * Compositor `NativeViewRef` into the real `addChildView`/`removeChildView` call. */ export interface ViewSubstrate { compositor: Compositor; windowScope: Scope; registerView(id: string, wcv: MinimalWebContentsView): void; /** Drop a disposed/detached view from this * substrate's registry + tracked z-order, so a long-lived window doesn't * accumulate dead views. Safe to call twice (Map.delete + guarded splice). */ unregisterView(id: string): void; } /** * Framework-internal "app" object —— `electronDeck(config)` 顶层入口的 plain-class * 形态,便于测试驱动 lifecycle 转换。加 wireTransport 注入 * 后可接真 ipcMain;加 electron 注入后可装配 mainWindow / toolbarView * / declared windows。两者都不注入时退化为同进程内存 fake。 * * @internal */ export declare class DeckApp { readonly config: DeckConfig; private readonly lifecycle; private readonly registry; private readonly bus; private readonly ipc; private readonly fwListeners; private readonly trustSet; /** Privileged-command grant registry. The policy gates * ControlBus.dispatch; grants are minted via `runtime.grants.issue`. */ private readonly capability; /** The grant-gated command bus for PRIVILEGED `layout.*` * commands. Constructed in `bindWireTransport` with the capability policy * injected, so `dispatch` default-DENIES any command lacking a live grant. * Privileged commands are registered via `runtime.layout.command`; ordinary * domain APIs stay on the un-gated `InMemoryTypedIpcRegistry`. */ private controlBus; /** Per-webContents backend `onWindowTrusted` Disposable, so a window's * trust mirror is undone when THAT window closes (not only at teardown). */ private readonly backendTrustDisposables; private readonly options; private wireTransport; /** The live wire senderPolicy, reused by buildRuntime so * `context._senderPolicy` reflects real trust instead of a `() => true` stub. */ private wireSenderPolicy; private mainWindow; private toolbarView; private readonly declaredWindows; private readonly trackedWindows; /** * The root lifetime scope of the app + a shadow map mirroring `trackedWindows`, * keyed by each window's webContents, carrying a per-window child Scope. The * shadow's key set is kept in lock-step with `trackedWindows` at every * maintenance point. */ private readonly rootScope; private readonly lifetimeShadow; /** * Per-trusted-webContents trust record. `wcScope` is a * child of the owning window's `windowScope`; it OWNS the wc's trust ref-count * lease(s). When the window closes, `windowScope.close()` cascades into this * `wcScope` (children-first LIFO), disposing every lease → ref-count hits 0 → * the wc leaves the trust set. * A wc can be trusted more than once (framework auto-trust + host * `windows.trust`), so `leases` is a Set. */ private readonly wcRecords; private readonly pendingWindowCreated; private readonly pendingLoadFailed; /** * Per-window native-view substrate, keyed by the window's * webContents (same key discipline as `lifetimeShadow`). Created at both * window-construction sites; dropped in `handleSubWindowClosed`. */ private readonly windowSubstrates; /** * Per-adopted-window registration handle, keyed by the adopted window's * webContents. `runtime.windows.adopt` is idempotent by wc identity: a second * adopt of the same window returns this stored Disposable (no double-admit, no * second substrate). The entry is removed when the registration is disposed * (early un-adopt) or the window's windowScope closes. */ private readonly adoptedWindows; /** * Per-registered-window {@link DeckWindow} facade + its per-window * close deciders, keyed by the window's control wc. Populated by * {@link registerWindow} for framework-created and adopted windows, plus the * framework-built main window. `deciders` is an ORDERED list run in * registration order on a close attempt; `closing` is the per-window in-flight * decision latch (mirrors the main-window `closingDecisionPromise`). */ private readonly windowRegistrations; /** The framework-built main window's control wc, so `runtime.windows.main` * can resolve its {@link DeckWindow}. Null under an `ownsWindows:true` backend. */ private mainControlWc; /** Monotonic id source for `runtime.view` native views. */ private viewSeq; /** * Provenance map for `runtime.scopes.create()` sessions: maps an opaque * {@link DeckSession} to its internal `rootScope.child()` Scope. A WeakMap so a * dropped session is GC'd; the framework holds the scope's lifetime via * rootScope anyway (the session scope is a rootScope child). `runtime.view` * resolves a passed session through this map — a foreign/raw Scope is absent * and therefore REJECTED. */ private readonly sessions; /** * keepAlive「opt-in helper:runtime.view({ keepAlive })」 — opt-in per-group LRU of HIDDEN keep-alive views. Group key is * `lru:${max}` (all `keepAlive:{policy:'lru',max:N}` views share one group per * `max`). Each group holds an ORDERED list of HIDDEN view ids (front = least * recently visible = first to evict) + a map from view id to its host handle so * an eviction can dispose it (→ its WebContents is destroyed). Views created * without `keepAlive` never participate. */ private readonly keepAliveGroups; /** Wcs that already have the `did-start-navigation` grant-reset hook bound, * so {@link bindNavigationGrantReset} is idempotent — a wc admitted to trust * more than once (e.g. constructed `autoTrust:false` then `windows.trust()`ed, * or un-adopted then re-adopted) never accumulates duplicate nav listeners. */ private readonly navHookBound; /** * slot-token registry (view-handle.md「slot-token 握手」/ capability-and-lifecycle.md「anchor slotToken 原子下发」): * each anchored `placeIn` mints an unguessable token bound to (viewId, slotId, * authorizedWcId, zone). The `__electron-deck:snapshot` apply path authorizes * each of the renderer's per-view tokens (token known AND authorized to the * sender wc), DERIVES the view's identity (viewId) + z-order (zone→layer) from * this registry (never trusting the renderer-reported fields), reconciles the * cleaned window-level table, and drives each view's `apply`. `resend` re-pushes * the slot-grant (layout-subscribe replay). */ private readonly slotTokens; private slotSeq; /** * Per-control-wc level-triggered reconciler state (keyed by authorizedWcId). * The renderer publishes a whole window-level desired-placement table each * frame; `reconcile` diffs it against the last-applied actual and emits ops, so * a lost or spurious per-view edge self-corrects instead of sticking a view * detached (the white-screen failure mode). Reset on layout-subscribe (reload) * and dropped on wc revoke. */ private readonly reconcileStates; /** * Per-control-wc strictly-monotonic generation counter. Bumped on every * layout-subscribe and stamped into each slot-grant, so a reload's higher * generation resets the reconciler regardless of IPC ordering (a late in-flight * pre-reload snapshot carries a lower generation → rejected). */ private readonly perWcGeneration; private _runtime; private startCalled; private shutdownPromise; /** Set when shutdown is driven by the `will-quit` handler — the app is already * quitting, so `doShutdown()` must NOT re-`app.quit()` (re-entrant quit). */ private quitInitiated; /** Close machine: non-null while a close decision is awaiting (in-flight latch). */ private closingDecisionPromise; /** Close machine: set once a 'close' decision is committed (guards the * window between decision-resolve and the 'closed' event). Never resets. */ private shuttingDown; /** Idempotency latch for {@link runShutdownCleanup}: the cleanup body * (beforeClose → backend.onShutdown → rootScope.close) must run AT MOST ONCE. * Both `doShutdown` (single-flight via shutdownPromise) AND `cleanupOnError` * (which bypasses that promise) reach here; and `rootScope.close()` inside the * body can synchronously fire a framework window's `'closed'` → `shutdown()` * re-entry. Without this latch that re-entry would invoke `backend.onShutdown` * a second time. * * This is a JOIN, not a boolean early-return: a re-entrant caller AWAITS the * SAME in-flight cleanup promise instead of returning immediately. A plain * `if (ran) return` let a concurrent shutdown skip past an in-flight cleanup * and force `app.quit()` mid-teardown (truncation). Never resets. */ private cleanupPromise; constructor(config: DeckConfig, options?: DeckAppOptions); get phase(): LifecyclePhase; get runtime(): Runtime; start(): Promise; /** * half-state guard: electron + (toolbar | windows) without wireTransport * would leave the host with webviews that can never reach back via * __electron-deck:invoke. Reject early with a clear msg. */ private assertWireTransportPresentForWebviewContent; /** * Pre-`whenReady` sequencing: single-instance lock (opt-in, must quit * BEFORE any other side effect) → `backend.beforeReady` (or the framework's * best-effort `app.setName`) → `await app.whenReady()` → app-level lifecycle * bindings. Returns `false` when the single-instance lock was lost (the * second instance already called `app.quit()` — `start()` must stop with * phase still `init`), `true` otherwise. */ private runPreReadyGate; /** * Single-instance gate (opt-in): a second instance must quit BEFORE any * side effect (beforeReady bootstrap / whenReady / window). Returns `true` * when the caller should continue (not opted in, or this instance holds the * lock); `false` after the losing instance has called `app.quit()`. */ private acquireSingleInstanceLock; /** Init → Bind → Setup → Ready. Assembly/bind must run AFTER the * whenReady gate (handled by the caller); loadURL/loadFile must run * AFTER bindWireTransport (a preload calling the bridge before its * ipcMain handler is registered would see a "no handler" reject). */ private runBindAndSetupPhases; /** * Process-level Electron lifecycle bindings (post-whenReady, independent of * `ownsWindows` — these are app events, not window events): * - `will-quit` → framework teardown (idempotent via `shutdownPromise`). * - `window-all-closed` → bound ONLY when `quitOnAllWindowsClosed` is set * (opt-in; omitted leaves Electron's default / the consumer's own handler). * - `second-instance` → backend hook, bound only under `singleInstance`. */ private bindAppLifecycle; shutdown(): Promise; /** * @internal windows.trust() / framework 内部添加 trusted webContents. * Backend-owned / untracked-window fallback: there is no framework windowScope * for these (the backend manages their lifetime), so the trust lease is owned * by `rootScope` as an app-shutdown backstop. The backend still disposes the * returned handle early when it untrusts / destroys its own window. */ _trustWebContents(wc: MinimalWebContents): Disposable; /** * Admit `wc` to the trust set under `windowScope`. Gets * or creates the wc's `wcScope` (a child of `windowScope`), takes a fresh * `trustSet.admit(wc, wcScope)` ref-count lease OWNED BY that wcScope, and returns a * one-shot host-facing Disposable that releases just THIS lease early. On * `wcScope.close()` (window-close cascade) every still-held lease is disposed * → ref-count zeroes → the wc leaves the set (driven by Scope teardown, * covering partially-built windows too). Idempotent registry cleanup is owned * by the wcScope. */ private admitTrust; /** * Bind the main-frame cross-document navigation grant-reset * hook on a control `wc`. On a MAIN-FRAME CROSS-DOCUMENT navigation * (`isMainFrame && !isInPlace`) this SYNCHRONOUSLY revokes the wc's capability * grants (`capability.revokeBySenderId`) and slot tokens, so the navigated-to * document can't inherit the prior page's privileges. Trust is LEFT INTACT (the * wc stays the framework's control surface) — we deliberately do NOT tear down * its `wcScope`, because that would dispose the trust lease and force an async * re-admit gap that breaks `runtime.grants.issue`. In-place (hash/pushState) and * sub-frame navigations are ignored; the initial document load is a no-op (no * grants exist yet). * * Must be called AFTER {@link admitTrust} so the wc's `wcScope` exists. The * `wc.on` guard tolerates the minimal fakes that lack an EventEmitter surface. */ private bindNavigationGrantReset; /** * Build the {@link DeckWindow} facade over a registered window and record * it (so `runtime.windows.create()` / `runtime.windows.main` can return it and * the per-window close machine can find its deciders). `newSession()` mints a * window-rooted {@link DeckSession} (a `windowScope.child()` registered in the * SAME provenance WeakMap as `runtime.scopes.create()`); `onClose()` appends a * per-window decider. */ private buildDeckWindow; /** * Resolve a window argument that may be either a raw `BrowserWindow` (the * historical `runtime.view().placeIn(window)` shape) OR a {@link DeckWindow} * handle (the `runtime.windows.create()` return). A DeckWindow exposes the * underlying window via `.window` and has no `.webContents` of its own; a raw * window has `.webContents`. Unwrap so the substrate lookup keys off the real * control webContents either way — keeping the `placeIn(deckWindow)` and * `placeIn(deckWindow.window)` call shapes both valid (additive, no break). */ private resolveWindowArg; /** * Run a registered window's per-window close deciders (registration order; * any `'keep'` vetoes), with an in-flight latch so a re-entrant close during a * pending decision decides ONCE. A throw / rejection fails CLOSED. Returns the * committed decision, or `null` when the decision is still in flight / there are * no deciders (caller falls back). Mirrors the main-window decision machine. */ private runWindowCloseDeciders; /** * Arm the per-window cancelable close-decision machine on a CREATED / * adopted window. On a `close` attempt: `preventDefault`, dispatch the * per-window deciders behind an in-flight latch (re-entrant closes swallowed), * and on a `'close'` decision `win.destroy()` (→ `'closed'` → revoke + * windowScope cascade). A `'keep'` clears the latch so the next close re-decides. * Only armed when at least one decider can exist (i.e. the window has a * registration) — windows with no per-window deciders fall through to Electron's * default close (the framework does not preventDefault). */ private armSubWindowCloseMachine; /** * Remove a previously-registered window event listener (Electron's * `removeListener`/`off`). Tolerates minimal test fakes that expose neither by * degrading to a no-op — the caller's ownership contract is best-effort cleanup. */ private removeWindowListener; /** * @internal Lifetime accessors. The live shadow map mirroring `trackedWindows`, * the root Scope, and a consistency assertion. */ __lifetimeShadow(): Map; /** @internal The root lifetime Scope. */ __rootScope(): Scope; /** @internal The per-trusted-wc trust records. */ __wcRecords(): Map; windowScope: Scope; }>; /** @internal The live capability policy (grant gate reads it). */ __capabilityPolicy(): CapabilityPolicy; /** * @internal Lifetime invariant: the shadow's per-window set * (the `window` of each entry) must equal `trackedWindows` membership. Throws * if violated; no-op otherwise. */ __assertLifetimeConsistent(): void; private bindDeclarativeFields; /** * Build a per-window {@link ViewSubstrate}. The * `ContentViewHost` adapts `win.contentView` and tracks the Compositor-managed * z-order in `order` (the minimal `contentView` has no `children()`). The * substrate's `detachAll` is owned on `windowScope` AFTER the window's * `win.destroy` own, so LIFO teardown runs detachAll BEFORE destroy. */ private createWindowSubstrate; private assembleElectron; /** 在 wireTransport.start 之后再 loadURL/loadFile,避免 preload 先于 ipcMain handler 注册触发 invoke。 */ private loadAssembledSources; /** * Load is best-effort: log + emit `load-failed` if it rejects, * but never let start() reject because of a renderer load issue (we'd * leave the host blocked indefinitely while the framework is otherwise * happy). */ private safeLoad; /** * D1 fix: load-failed 在 listener 注册之前到达时 buffer 到 pendingLoadFailed; * 第一个 listener 注册时 splice 消费整个队列。已有 listener 时直接 emit。 */ private surfaceLoadFailed; private bindWireTransport; /** * The two-route boundary's privileged-name predicate (「两条 invoke 路由的硬边界」). A * PRIVILEGED command name — by convention `layout.*` — routes through the * grant-gated {@link ControlBus} (`controlBus.dispatch`). Ordinary domain * APIs (any other name) stay on the un-gated declarative `hostServices` * route. Privileged names MUST NOT be registered in `hostServices`. */ private isPrivilegedCommandName; /** * Warn ONCE about an invalid `keepAlive.max` (negative / non-integer / * NaN). Such a view is not keep-alive-managed (the group is skipped); warning * once avoids log spam when many views share the same bad config. */ private warnedInvalidKeepAliveMax; private warnInvalidKeepAliveMax; /** Drop `viewId` from `groupKey`'s HIDDEN list only (the group's `handles` * entry stays — the view is still keep-alive-managed, just no longer * evictable while visible). Idempotent: a no-op when the view isn't on the * hidden list or the group doesn't exist. */ private dropViewFromKeepAliveHidden; private constructWindow; /** * Register an EXTERNALLY-created window into the framework so * `runtime.view().placeIn(win)` works for it: a `rootScope.child()` * windowScope, a per-window {@link ViewSubstrate}, and the TRUST lifecycle — * mirroring {@link constructWindow}'s registration, but for a window the host * built (e.g. under `ownsWindows:true`). The framework holds NO windowScope / * substrate / trust for such a window until this call. * * Ordering MIRRORS the framework's own windows (constructWindow / main-window): * 1) build the windowScope (child of rootScope so shutdown cascades it), * 2) build + register the per-window substrate (so placeIn can resolve it), * 3) for `ownership:'transfer'` ONLY, OWN `() => win.destroy()` on the * windowScope so app shutdown destroys the window; for `'observe'` the host * keeps lifetime control (the framework never destroys it), * 4) arm trust+grant+slot-token revocation as the FIRST `'closed'` listener via * `prependListener` (runs before any external `'closed'` listener), * 5) `admitTrust` AFTER the revoke listener is registered (constructWindow order). * * Idempotent by webContents identity: a second adopt of the same window returns * the existing registration. Throws if the window/webContents is destroyed. */ private adoptWindow; /** * Synchronously revoke BOTH trust leases AND capability * grants for every wc admitted under `windowScope` (the window's control wc + * any siblings like the toolbar wc). Called from the window's 'closed' handler * so both authorizations are gone the instant the window is destroyed, * closing every async-cascade race (a destroyed wc can never be observed trusted OR granted * by a later shutdown/beforeClose, nor by a NEW window that reuses the same * wc.id before the async scope cascade revokes it). The leases are ALSO owned * by their wcScope, so the async `windowScope.close()` still runs (it disposes * the already-disposed leases idempotently, plus win.destroy + wcRecords * cleanup) and remains the teardown/partial-init fallback for paths where no * 'closed' fires (cleanupOnError, shutdown of a never-closed window). * `lease.dispose()` is synchronous (trustSet ref-count--) and one-shot, so no * double-decrement; `revokeBySenderId` is likewise idempotent, so the grant's * own async `senderScope.on('closed')` revoke later becomes a no-op. */ private revokeWindowTrust; /** * Arm the wire's slot-token inbound channels (idempotent). Called once at * framework start() (bindWireTransport, right after * transport.start()) so a layout client can subscribe before the first * anchored placeIn; the anchored `placeIn` call site is now a redundant no-op * (kept for safety). No-op when there's no wire transport (main-internal-only * builds can't serve a renderer slot anyway). */ private ensureSlotChannelsArmed; /** * slot-token apply path (`__electron-deck:snapshot`). The renderer publishes a * whole window-level desired-placement table; each entry's slotToken is the * credential. Steps: * 1. AUTHORIZE + clean: `cleanSnapshot` keeps only views whose token is known * AND authorized to `senderId` (anti-spoof), deriving each view's identity * (viewId) + z-order (zone→layer) from the registry — never from the * renderer-reported fields. A non-empty snapshot that fully fails * authorization is rejected wholesale (returns null) so a transient token * failure is NOT read as "detach everything". * 2. RECONCILE the cleaned table against this wc's last-applied actual state * → an ordered op list (level-triggered; a lost/spurious frame self-heals). * 3. DISPATCH the ops onto each view's `applyPlacement` sink. */ private handleSnapshot; /** * Per-wc layout-subscribe replay (`__electron-deck:layout-subscribe`). When the * authorized control wc (re)subscribes (e.g. after reload), BUMP its generation * and RESET its reconciler (a reload restarts the renderer's epoch at 0; without * the reset the fresh low-epoch snapshots would be rejected as stale), then * re-push every grant it owns — now carrying the bumped generation — so it can * re-bind its DOM slots. A DIFFERENT wc's subscribe never touches another wc's * state or grants. */ private handleLayoutSubscribe; /** Drop every slot token + reconciler/generation state authorized to `wcId` * (window-close / shutdown hygiene). */ private revokeSlotTokensForWc; private handleSubWindowClosed; /** * Notify the backend that the framework has edge-trusted one of its * (framework-built) webContents, so the backend can mirror it into the domain * trust set. The returned Disposable is disposed when THAT window closes * ({@link handleSubWindowClosed}) — honouring the contract "framework disposes * on untrust / window destroy" — and is also added to the registry as a * teardown safety net (e.g. the main window, which dies with the app). The * wrapper is one-shot so the two paths don't double-dispose. */ private _notifyBackendTrusted; private emitFrameworkEvent; private cleanupOnError; private doShutdown; private runShutdownCleanup; private doShutdownCleanup; private buildRuntime; } //# sourceMappingURL=deck-app.d.ts.map