@dimina-kit/devtools 0.3.2-dev.20260611135124 → 0.4.0-dev.20260612152115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/main/api.d.ts +1 -1
- package/dist/main/app/app.js +22 -12
- package/dist/main/app/launch.d.ts +4 -4
- package/dist/main/app/launch.js +5 -17
- package/dist/main/index.bundle.js +372 -225
- package/dist/main/ipc/bridge-router.js +35 -8
- package/dist/main/ipc/views.js +10 -1
- package/dist/main/menu/index.d.ts +2 -2
- package/dist/main/menu/index.js +4 -2
- package/dist/main/runtime/miniapp-runtime.d.ts +79 -11
- package/dist/main/services/notifications/renderer-notifier.d.ts +11 -0
- package/dist/main/services/notifications/renderer-notifier.js +3 -0
- package/dist/main/services/views/host-toolbar-port-channel.d.ts +10 -0
- package/dist/main/services/views/host-toolbar-port-channel.js +89 -10
- package/dist/main/services/views/view-manager.d.ts +28 -4
- package/dist/main/services/views/view-manager.js +42 -8
- package/dist/main/services/workbench-context.d.ts +7 -0
- package/dist/main/services/workbench-context.js +5 -0
- package/dist/main/services/workspace/workspace-service.d.ts +3 -7
- package/dist/main/services/workspace/workspace-service.js +66 -0
- package/dist/main/windows/settings-window/index.d.ts +33 -0
- package/dist/main/windows/settings-window/index.js +59 -0
- package/dist/preload/index.d.ts +2 -2
- package/dist/preload/index.js +2 -2
- package/dist/preload/runtime/host-toolbar-port.d.ts +10 -5
- package/dist/preload/runtime/host-toolbar-port.js +24 -0
- package/dist/preload/windows/host-toolbar-runtime.cjs +20 -1
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs +9 -7
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/renderer/assets/index-Bi_6SS-L.js +50 -0
- package/dist/renderer/assets/{input-6fpCXV-n.js → input-DgqX2UvN.js} +2 -2
- package/dist/renderer/assets/{ipc-transport-D22e4dv7.js → ipc-transport-CsFhDJ7q.js} +2 -2
- package/dist/renderer/assets/ipc-transport-SnOAk0Jq.css +1 -0
- package/dist/renderer/assets/{popover-CdGdKkkc.js → popover-D_2o_52M.js} +2 -2
- package/dist/renderer/assets/{select-X0sbjwdP.js → select-BJYsdghn.js} +2 -2
- package/dist/renderer/assets/{settings-CS4jYaCw.js → settings-CivkfizP.js} +2 -2
- package/dist/renderer/assets/{settings-api-CCWTAao_.js → settings-api-7mXkBy5-.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-Bpzaj3bm.js → workbenchSettings-BwpwxuzM.js} +2 -2
- package/dist/renderer/entries/main/index.html +6 -6
- package/dist/renderer/entries/popover/index.html +5 -5
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +4 -4
- package/dist/shared/ipc-channels.d.ts +11 -0
- package/dist/shared/ipc-channels.js +14 -0
- package/dist/shared/types.d.ts +48 -11
- package/dist/simulator/assets/bridge-channels-BUQ5AbvJ.js +2 -0
- package/dist/simulator/assets/device-shell-BEnc2k-W.js +2 -0
- package/dist/simulator/assets/{simulator-DHylZf9Z.js → simulator-cV1PERi6.js} +3 -3
- package/dist/simulator/assets/simulator-mini-app-CYtW6bMd.js +2 -0
- package/dist/simulator/simulator.html +1 -1
- package/package.json +4 -4
- package/dist/renderer/assets/index-DLzjz1wv.js +0 -50
- package/dist/renderer/assets/ipc-transport-Bs8Sf1B2.css +0 -1
- package/dist/simulator/assets/device-shell-fH2cI-3q.js +0 -2
- package/dist/simulator/assets/simulator-mini-app-BDNu5n26.js +0 -2
|
@@ -58,17 +58,26 @@ const API_CALL_TIMEOUT_MS = 5_000;
|
|
|
58
58
|
* rather than assuming a single session.
|
|
59
59
|
*/
|
|
60
60
|
function resolveCurrentApp(state, ctx, appId) {
|
|
61
|
+
// Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
|
|
62
|
+
// order): after a respawn/reopen the newest session is the live one — the
|
|
63
|
+
// first match could be a just-superseded session mid-teardown.
|
|
61
64
|
if (appId) {
|
|
65
|
+
let match;
|
|
62
66
|
for (const ap of state.appSessions.values())
|
|
63
67
|
if (ap.appId === appId)
|
|
64
|
-
|
|
68
|
+
match = ap;
|
|
69
|
+
if (match)
|
|
70
|
+
return match;
|
|
65
71
|
}
|
|
66
72
|
const appInfo = ctx.workspace?.getSession?.()?.appInfo;
|
|
67
73
|
const activeAppId = appInfo?.appId;
|
|
68
74
|
if (activeAppId) {
|
|
75
|
+
let match;
|
|
69
76
|
for (const ap of state.appSessions.values())
|
|
70
77
|
if (ap.appId === activeAppId)
|
|
71
|
-
|
|
78
|
+
match = ap;
|
|
79
|
+
if (match)
|
|
80
|
+
return match;
|
|
72
81
|
}
|
|
73
82
|
// Maps preserve insertion order; the last entry is the most recent spawn.
|
|
74
83
|
let last;
|
|
@@ -87,6 +96,12 @@ export function installBridgeRouter(ctx) {
|
|
|
87
96
|
emitRenderEvent: () => { },
|
|
88
97
|
connections: ctx.connections,
|
|
89
98
|
debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
|
|
99
|
+
evictAppDataBridges: (ap) => {
|
|
100
|
+
if (!ctx.appData)
|
|
101
|
+
return;
|
|
102
|
+
for (const page of ap.pages.values())
|
|
103
|
+
ctx.appData.evictBridge(ap.appId, page.bridgeId);
|
|
104
|
+
},
|
|
90
105
|
};
|
|
91
106
|
// Opt-in (default OFF) pre-warm pool for service-host windows. When enabled,
|
|
92
107
|
// handleSpawn acquires a warm window instead of constructing one per spawn.
|
|
@@ -298,12 +313,8 @@ export function installBridgeRouter(ctx) {
|
|
|
298
313
|
console.warn(`[bridge-router] DISPOSE rejected: sender belongs to ${senderApp.appSessionId}, target ${target.appSessionId}`);
|
|
299
314
|
return;
|
|
300
315
|
}
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
if (ctx.appData) {
|
|
304
|
-
for (const page of target.pages.values())
|
|
305
|
-
ctx.appData.evictBridge(target.appId, page.bridgeId);
|
|
306
|
-
}
|
|
316
|
+
// AppData bridge eviction happens inside disposeAppSession (single
|
|
317
|
+
// chokepoint shared with the simulator-WCV 'destroyed' path).
|
|
307
318
|
void disposeAppSession(state, target.appSessionId);
|
|
308
319
|
};
|
|
309
320
|
ipcMain.on(C.DISPOSE, onDispose);
|
|
@@ -517,6 +528,19 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
517
528
|
};
|
|
518
529
|
appSession.onServiceClosed = onServiceClosed;
|
|
519
530
|
serviceWindow.once('closed', onServiceClosed);
|
|
531
|
+
// The simulator WCV owns the app's UI lifetime. When it is destroyed —
|
|
532
|
+
// project close (`views.disposeAll`/detach) or a DeviceShell respawn
|
|
533
|
+
// (`attachNativeSimulator`, e.g. watcher hot reload) — the guest never gets
|
|
534
|
+
// to send its graceful `C.DISPOSE`, so without this hook the app session and
|
|
535
|
+
// its hidden service-host window leak, and `resolveCurrentApp` keeps
|
|
536
|
+
// resolving the STALE session for the same appId: `getActiveRenderWc` then
|
|
537
|
+
// dereferences dead pages and every panel pull (WXML/elements) returns null
|
|
538
|
+
// for the whole next session. Disposing here is idempotent with the graceful
|
|
539
|
+
// path (`disposeAppSession` early-returns once the session is gone).
|
|
540
|
+
const onSimulatorDestroyed = () => {
|
|
541
|
+
void disposeAppSession(state, appSessionId);
|
|
542
|
+
};
|
|
543
|
+
simulatorWc.once('destroyed', onSimulatorDestroyed);
|
|
520
544
|
if (usedPool) {
|
|
521
545
|
// A pooled/fallback window passes through about:blank (warm load or the
|
|
522
546
|
// fallback's initial load). Boot only once the REAL service.html navigation
|
|
@@ -1144,6 +1168,9 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1144
1168
|
if (!ap)
|
|
1145
1169
|
return;
|
|
1146
1170
|
state.appSessions.delete(appSessionId);
|
|
1171
|
+
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1172
|
+
// page teardown below progressively empties (and finally clears).
|
|
1173
|
+
state.evictAppDataBridges(ap);
|
|
1147
1174
|
// Drain any pending API calls owned by this app session. One-shot calls
|
|
1148
1175
|
// normally self-clean on response/timeout, but persistent (`keep: true`)
|
|
1149
1176
|
// subscriptions (e.g. audioListen) live with their timer cleared until
|
package/dist/main/ipc/views.js
CHANGED
|
@@ -30,7 +30,16 @@ export function registerViewsIpc(ctx) {
|
|
|
30
30
|
.handle(ViewChannel.HostToolbarBounds, (_event, ...args) => {
|
|
31
31
|
const [bounds] = validate(ViewChannel.HostToolbarBounds, ViewBoundsSchema, args);
|
|
32
32
|
ctx.views.setHostToolbarBounds(bounds);
|
|
33
|
-
})
|
|
33
|
+
})
|
|
34
|
+
// Height replay pull: a freshly-mounted main-renderer placeholder asks for
|
|
35
|
+
// the last NOTIFIED toolbar height (main retains it — the toolbar's
|
|
36
|
+
// size-advertiser deduplicates and never re-pushes, so a push that fired
|
|
37
|
+
// while no project view was mounted is otherwise lost: cold start races
|
|
38
|
+
// it, close-project → reopen hits it always). Rides the SAME
|
|
39
|
+
// senderPolicy-gated registry as HostToolbarBounds: the toolbar WCV's
|
|
40
|
+
// arbitrary host content must not reach this — only the trusted main
|
|
41
|
+
// renderer pulls. Live delegation, not a registration-time snapshot.
|
|
42
|
+
.handle(ViewChannel.HostToolbarGetHeight, () => ctx.views.getHostToolbarHeight());
|
|
34
43
|
// Reverse size-advertiser: the toolbar WCV's OWN renderer sends this, and the
|
|
35
44
|
// host loads ARBITRARY content into that WCV. We DELIBERATELY do NOT add the
|
|
36
45
|
// toolbar wc to the global sender policy — that would trust it for ALL ~72
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function installAppMenu(ctx:
|
|
1
|
+
import type { MenuContext } from '../../shared/types.js';
|
|
2
|
+
export declare function installAppMenu(ctx: MenuContext): void;
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/main/menu/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Menu } from 'electron';
|
|
2
|
-
|
|
2
|
+
// The built-in menu consumes the narrow MenuContext, the same surface a host
|
|
3
|
+
// menuBuilder receives — proof that the hand-written contract covers the
|
|
4
|
+
// real internal consumption (settings entry + navigate-back).
|
|
3
5
|
export function installAppMenu(ctx) {
|
|
4
6
|
const template = [
|
|
5
7
|
{
|
|
@@ -8,7 +10,7 @@ export function installAppMenu(ctx) {
|
|
|
8
10
|
{
|
|
9
11
|
label: '开发工具设置',
|
|
10
12
|
click: () => {
|
|
11
|
-
void
|
|
13
|
+
void ctx.openSettings().catch(() => { });
|
|
12
14
|
},
|
|
13
15
|
},
|
|
14
16
|
{ type: 'separator' },
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
* past the drift sentinel.
|
|
18
18
|
* - `workspace.openProject` stays writable (no `readonly`): qdmp gates
|
|
19
19
|
* project permissions by reassigning it (documented monkey-patch contract).
|
|
20
|
-
* - `windows` is an OPAQUE handle (`object`): hosts pass it through to
|
|
21
|
-
* framework helpers but never reach into it (no BrowserWindow leak).
|
|
22
20
|
*
|
|
23
21
|
* `asMiniappRuntime(ctx)` is an identity return — the contract is a typed
|
|
24
22
|
* VIEW onto the live context, not a snapshot/projection. That is what makes
|
|
@@ -35,6 +33,53 @@ export interface MiniappProjectStatusPayload {
|
|
|
35
33
|
/** True when the status update is emitted by the file-watcher rebuild loop. */
|
|
36
34
|
hotReload?: boolean;
|
|
37
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Structured shape of `getSession().appInfo` — the doc-promised
|
|
38
|
+
* (`host-migration.md` "appInfo 已结构化") DTO, named so hosts can type what
|
|
39
|
+
* they receive without deep imports or casts.
|
|
40
|
+
*
|
|
41
|
+
* `appId` is REQUIRED: the renderer derives its IPC scoping from it and the
|
|
42
|
+
* default devkit adapter always supplies one (fallback included). A custom
|
|
43
|
+
* adapter that returns a session without a string `appId` is rejected at the
|
|
44
|
+
* `openProject` boundary (see workspace-service.ts) — it never becomes the
|
|
45
|
+
* active session. The decorative rest stays optional: `name`/`path` come from
|
|
46
|
+
* the devkit adapter, `appName` from the devtools-side mirror; minimal custom
|
|
47
|
+
* adapters may omit all three.
|
|
48
|
+
*/
|
|
49
|
+
export interface MiniappSessionAppInfo {
|
|
50
|
+
appId: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
path?: string;
|
|
53
|
+
appName?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The page-side bridge the framework injects into the host-toolbar WCV as
|
|
57
|
+
* `window.diminaHostToolbar` (see src/preload/runtime/host-toolbar-port.ts).
|
|
58
|
+
* Declared here (the electron-free contract module) so TypeScript toolbar
|
|
59
|
+
* pages can type the bridge without hand-rolling — and without importing
|
|
60
|
+
* anything Electron-flavored.
|
|
61
|
+
*
|
|
62
|
+
* Shape mirrors the injected bridge EXACTLY:
|
|
63
|
+
* - `send` returns void (pre-handshake sends are queued, bounded at 128);
|
|
64
|
+
* - `onMessage` returns a BARE un-subscribe function — unlike the main-side
|
|
65
|
+
* control's `{ dispose }` — because that is what the preload exposes.
|
|
66
|
+
* Both throw a `TypeError` synchronously when `channel` is not a non-empty
|
|
67
|
+
* string (parity with the main side's `onMessage` guard).
|
|
68
|
+
*/
|
|
69
|
+
export interface DiminaHostToolbarPageBridge {
|
|
70
|
+
send: (channel: string, payload: unknown) => void;
|
|
71
|
+
onMessage: (channel: string, handler: (payload: unknown) => void) => () => void;
|
|
72
|
+
}
|
|
73
|
+
declare global {
|
|
74
|
+
interface Window {
|
|
75
|
+
/**
|
|
76
|
+
* Present ONLY inside the host-toolbar WebContentsView's main frame
|
|
77
|
+
* (guarded injection — see host-toolbar-runtime.ts); optional everywhere
|
|
78
|
+
* else, so page code must runtime-guard before use.
|
|
79
|
+
*/
|
|
80
|
+
diminaHostToolbar?: DiminaHostToolbarPageBridge;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
38
83
|
/**
|
|
39
84
|
* Host-facing control surface for the toolbar WebContentsView — the exact
|
|
40
85
|
* post-R2 message-channel surface (`send`/`onMessage`), with no `webContents`
|
|
@@ -67,9 +112,22 @@ export interface MiniappHostToolbar {
|
|
|
67
112
|
onMessage: (channel: string, handler: (payload: unknown) => void) => {
|
|
68
113
|
dispose: () => void;
|
|
69
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* Observe handshake readiness. Fires the handler once per load generation,
|
|
117
|
+
* exactly when the toolbar page's MessagePort handshake completes (i.e.
|
|
118
|
+
* when `send` flips to true). Registering while the channel is already
|
|
119
|
+
* ready fires once asynchronously on a microtask (missed-signal guard);
|
|
120
|
+
* a reload / re-handshake fires registered handlers again. `dispose()`
|
|
121
|
+
* detaches (idempotent).
|
|
122
|
+
*/
|
|
123
|
+
onReady: (handler: () => void) => {
|
|
124
|
+
dispose: () => void;
|
|
125
|
+
};
|
|
70
126
|
/**
|
|
71
127
|
* Pin (`{ fixed }`) or unpin (`'auto'`) the toolbar strip height. `'auto'`
|
|
72
128
|
* (default) lets the in-page height advertiser drive the placeholder.
|
|
129
|
+
* `{ fixed }` values must be finite and non-negative — anything else throws
|
|
130
|
+
* a `TypeError` synchronously and leaves the standing mode untouched.
|
|
73
131
|
*/
|
|
74
132
|
setHeightMode: (mode: 'auto' | {
|
|
75
133
|
fixed: number;
|
|
@@ -103,10 +161,11 @@ export interface MiniappWorkspace {
|
|
|
103
161
|
/**
|
|
104
162
|
* Minimal live-session DTO, or null when no session. Deliberately excludes
|
|
105
163
|
* the session's own `close` — hosts end sessions via `closeProject`, never
|
|
106
|
-
* behind the workspace's back.
|
|
164
|
+
* behind the workspace's back. `appInfo` is structured (see
|
|
165
|
+
* {@link MiniappSessionAppInfo}) — no casting required.
|
|
107
166
|
*/
|
|
108
167
|
getSession: () => {
|
|
109
|
-
appInfo:
|
|
168
|
+
appInfo: MiniappSessionAppInfo;
|
|
110
169
|
} | null;
|
|
111
170
|
}
|
|
112
171
|
/**
|
|
@@ -115,8 +174,6 @@ export interface MiniappWorkspace {
|
|
|
115
174
|
* plumbing is a deliberate semver decision, not an accident of projection.
|
|
116
175
|
*/
|
|
117
176
|
export interface MiniappRuntime {
|
|
118
|
-
/** Absolute path to the devtools renderer dist directory. */
|
|
119
|
-
rendererDir: string;
|
|
120
177
|
/** View layer — only the host-owned toolbar control is public. */
|
|
121
178
|
views: {
|
|
122
179
|
readonly hostToolbar: MiniappHostToolbar;
|
|
@@ -127,15 +184,26 @@ export interface MiniappRuntime {
|
|
|
127
184
|
notify: {
|
|
128
185
|
projectStatus: (payload: MiniappProjectStatusPayload) => void;
|
|
129
186
|
};
|
|
130
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Lifecycle sink: hosts register their own teardown via `registry.add`.
|
|
189
|
+
* Accepts BOTH disposal idioms — a `{ dispose }` object (what
|
|
190
|
+
* `hostToolbar.onMessage`/`onReady` return) or a bare `() => void` —
|
|
191
|
+
* matching the live registry, so `registry.add(sub)` works without the
|
|
192
|
+
* `registry.add(() => sub.dispose())` wrapper.
|
|
193
|
+
*/
|
|
131
194
|
registry: {
|
|
132
|
-
add: (
|
|
195
|
+
add: (d: {
|
|
196
|
+
dispose: () => void;
|
|
197
|
+
} | (() => void)) => void;
|
|
133
198
|
};
|
|
134
199
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
200
|
+
* Open (or re-focus) the standalone workbench-settings window. Replaces the
|
|
201
|
+
* retired `windows` opaque pass-through (whose only documented purpose —
|
|
202
|
+
* `openSettingsWindow(ctx)` — could never typecheck from the contract) and
|
|
203
|
+
* the `rendererDir` member that existed solely to feed it; hosts needing
|
|
204
|
+
* the renderer dist path use the `/paths` export instead.
|
|
137
205
|
*/
|
|
138
|
-
|
|
206
|
+
openSettings: () => Promise<void>;
|
|
139
207
|
}
|
|
140
208
|
/**
|
|
141
209
|
* View a full `WorkbenchContext` as its `MiniappRuntime` contract. Identity
|
|
@@ -12,6 +12,15 @@ export interface ProjectStatusPayload {
|
|
|
12
12
|
/** True when the status update is emitted by the file-watcher rebuild loop. */
|
|
13
13
|
hotReload?: boolean;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Payload for the `project:compileLog` push — one filtered dmcc log line.
|
|
17
|
+
* `at` is stamped in the main process when the line is captured.
|
|
18
|
+
*/
|
|
19
|
+
export interface CompileLogPayload {
|
|
20
|
+
at: number;
|
|
21
|
+
stream: 'stdout' | 'stderr';
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
15
24
|
/**
|
|
16
25
|
* Payload for the `settings:init` event sent into the embedded settings overlay
|
|
17
26
|
* right after it is shown.
|
|
@@ -43,6 +52,8 @@ export interface WorkbenchSettingsInitPayload {
|
|
|
43
52
|
export interface RendererNotifier {
|
|
44
53
|
/** Broadcast project compile status transitions to the main renderer. */
|
|
45
54
|
projectStatus(payload: ProjectStatusPayload): void;
|
|
55
|
+
/** Push one per-line dmcc compile-log entry to the main renderer. */
|
|
56
|
+
compileLog(payload: CompileLogPayload): void;
|
|
46
57
|
/** Ask the main renderer to navigate back to its landing screen. */
|
|
47
58
|
windowNavigateBack(): void;
|
|
48
59
|
/** Tell the main renderer the compile popover has been closed. */
|
|
@@ -24,6 +24,9 @@ export function createRendererNotifier(ctx) {
|
|
|
24
24
|
projectStatus(payload) {
|
|
25
25
|
sendToMain(ProjectChannel.Status, payload);
|
|
26
26
|
},
|
|
27
|
+
compileLog(payload) {
|
|
28
|
+
sendToMain(ProjectChannel.CompileLog, payload);
|
|
29
|
+
},
|
|
27
30
|
windowNavigateBack() {
|
|
28
31
|
sendToMain(WindowChannel.NavigateBack);
|
|
29
32
|
},
|
|
@@ -68,6 +68,16 @@ export interface HostToolbarPortChannel {
|
|
|
68
68
|
invalidate(): void;
|
|
69
69
|
/** Register a control-level inbound handler for `channel`. */
|
|
70
70
|
onMessage(channel: string, handler: (payload: unknown) => void): HostToolbarMessageSubscription;
|
|
71
|
+
/**
|
|
72
|
+
* Observe handshake readiness. Fires `handler` once per load GENERATION,
|
|
73
|
+
* at the moment that generation's handshake completes (`send` flips true).
|
|
74
|
+
* Registering while already `ready` schedules a one-shot catch-up fire on a
|
|
75
|
+
* microtask — never synchronously — and the catch-up RE-CHECKS at fire time
|
|
76
|
+
* that (a) the subscription is still registered and (b) the generation is
|
|
77
|
+
* unchanged / the port is still live, so a same-frame `dispose()` or
|
|
78
|
+
* host-initiated load suppresses it. Inert (never fires) after `dispose()`.
|
|
79
|
+
*/
|
|
80
|
+
onReady(handler: () => void): HostToolbarMessageSubscription;
|
|
71
81
|
/**
|
|
72
82
|
* Post `{ channel, payload }` to the toolbar page over the live port.
|
|
73
83
|
* Returns false (delivering NOTHING, creating NOTHING) when there is no
|
|
@@ -50,10 +50,20 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
50
50
|
let activePort = null;
|
|
51
51
|
/** The wc that owns `activePort` (so a stale wc's `destroyed` can't drop a successor's port). */
|
|
52
52
|
let activeWc = null;
|
|
53
|
-
|
|
53
|
+
/** Named lifecycle state. INVARIANT: `state === 'ready'` ⟺ `activePort !== null`. */
|
|
54
|
+
let state = 'absent';
|
|
55
|
+
/**
|
|
56
|
+
* Monotonic per-handshake counter. Each completed handshake is one load
|
|
57
|
+
* GENERATION; `onReady` catch-up fires capture it at registration and
|
|
58
|
+
* re-check it at fire time so a fire scheduled for generation N can never
|
|
59
|
+
* deliver after a navigation/handshake moved the channel past N.
|
|
60
|
+
*/
|
|
61
|
+
let generation = 0;
|
|
54
62
|
// Array (not Map<channel, Set>) so the same handler function may be
|
|
55
63
|
// registered twice and each registration disposes independently.
|
|
56
64
|
const handlers = [];
|
|
65
|
+
// onReady registrations. Same array-of-entries discipline as `handlers`.
|
|
66
|
+
const readyHandlers = [];
|
|
57
67
|
function dispatch(data) {
|
|
58
68
|
// Inbound waist: the toolbar page is host-arbitrary content — anything
|
|
59
69
|
// that is not an object envelope with a string channel is dropped.
|
|
@@ -68,10 +78,16 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
68
78
|
entry.handler(payload);
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Drop the live port and transition to `next` (`'absent'` for death paths,
|
|
83
|
+
* `'awaitingHandshake'` for navigation paths). Never demotes `disposed`.
|
|
84
|
+
*/
|
|
85
|
+
function dropActivePort(close, next) {
|
|
72
86
|
const port = activePort;
|
|
73
87
|
activePort = null;
|
|
74
88
|
activeWc = null;
|
|
89
|
+
if (state !== 'disposed')
|
|
90
|
+
state = next;
|
|
75
91
|
if (close && port) {
|
|
76
92
|
try {
|
|
77
93
|
port.close();
|
|
@@ -81,13 +97,39 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
81
97
|
}
|
|
82
98
|
}
|
|
83
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Invoke one onReady handler with exception ISOLATION: subscribers are
|
|
102
|
+
* arbitrary control-level code — one throwing must neither starve sibling
|
|
103
|
+
* registrations nor escape the surrounding event/microtask callback (a
|
|
104
|
+
* throw out of a `did-finish-load` listener or a `queueMicrotask` body is
|
|
105
|
+
* process-level `uncaughtException` territory). Report-and-continue.
|
|
106
|
+
*/
|
|
107
|
+
function invokeReadyHandler(handler) {
|
|
108
|
+
try {
|
|
109
|
+
handler();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.error('[host-toolbar] onReady handler threw:', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Fire every still-registered onReady handler exactly once (snapshot iteration). */
|
|
116
|
+
function fireReadyHandlers() {
|
|
117
|
+
for (const entry of [...readyHandlers]) {
|
|
118
|
+
// A handler may dispose a sibling registration mid-fire: re-check
|
|
119
|
+
// membership so a disposed entry never fires. Per-handler isolation
|
|
120
|
+
// (NOT around the loop): a throwing handler must not abort the fire
|
|
121
|
+
// for later-registered siblings.
|
|
122
|
+
if (readyHandlers.includes(entry))
|
|
123
|
+
invokeReadyHandler(entry.handler);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
84
126
|
function handshake(wc) {
|
|
85
|
-
if (disposed)
|
|
127
|
+
if (state === 'disposed')
|
|
86
128
|
return;
|
|
87
129
|
if (!opts.isCurrent(wc))
|
|
88
130
|
return;
|
|
89
131
|
// The previous load's document is gone; its port goes with it.
|
|
90
|
-
dropActivePort(true);
|
|
132
|
+
dropActivePort(true, 'awaitingHandshake');
|
|
91
133
|
const { port1, port2 } = new MessageChannelMain();
|
|
92
134
|
port1.on('message', (event) => {
|
|
93
135
|
dispatch(event.data);
|
|
@@ -96,12 +138,17 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
96
138
|
// the reference so send() reports false instead of posting into the void.
|
|
97
139
|
port1.on('close', () => {
|
|
98
140
|
if (activePort === port1)
|
|
99
|
-
dropActivePort(false);
|
|
141
|
+
dropActivePort(false, 'absent');
|
|
100
142
|
});
|
|
101
143
|
wc.postMessage(ViewChannel.HostToolbarPort, null, [port2]);
|
|
102
144
|
port1.start();
|
|
103
145
|
activePort = port1;
|
|
104
146
|
activeWc = wc;
|
|
147
|
+
state = 'ready';
|
|
148
|
+
generation++;
|
|
149
|
+
// Readiness signal AFTER the port is live: a handler calling send() from
|
|
150
|
+
// inside its onReady fire must observe `true`.
|
|
151
|
+
fireReadyHandlers();
|
|
105
152
|
}
|
|
106
153
|
return {
|
|
107
154
|
attach(wc) {
|
|
@@ -129,15 +176,15 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
129
176
|
: isMainFramePositional;
|
|
130
177
|
if (isSameDocument || !isMainFrame)
|
|
131
178
|
return;
|
|
132
|
-
dropActivePort(true);
|
|
179
|
+
dropActivePort(true, 'awaitingHandshake');
|
|
133
180
|
});
|
|
134
181
|
wc.on('destroyed', () => {
|
|
135
182
|
if (activeWc === wc)
|
|
136
|
-
dropActivePort(true);
|
|
183
|
+
dropActivePort(true, 'absent');
|
|
137
184
|
});
|
|
138
185
|
},
|
|
139
186
|
invalidate() {
|
|
140
|
-
dropActivePort(true);
|
|
187
|
+
dropActivePort(true, 'awaitingHandshake');
|
|
141
188
|
},
|
|
142
189
|
onMessage(channel, handler) {
|
|
143
190
|
if (typeof channel !== 'string' || channel === '') {
|
|
@@ -153,6 +200,37 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
153
200
|
},
|
|
154
201
|
};
|
|
155
202
|
},
|
|
203
|
+
onReady(handler) {
|
|
204
|
+
// Torn-down control: inert registration — never fires, never throws.
|
|
205
|
+
if (state === 'disposed') {
|
|
206
|
+
return { dispose() { } };
|
|
207
|
+
}
|
|
208
|
+
const entry = { handler };
|
|
209
|
+
readyHandlers.push(entry);
|
|
210
|
+
if (state === 'ready') {
|
|
211
|
+
// Missed-signal catch-up: the handshake already happened, so the
|
|
212
|
+
// subscriber would otherwise wait forever. Asynchronous on a
|
|
213
|
+
// microtask (never re-enter host code synchronously inside
|
|
214
|
+
// onReady()), and RE-CHECKED at fire time — both the subscription's
|
|
215
|
+
// liveness and the load generation can change between scheduling and
|
|
216
|
+
// the microtask (same-frame dispose() / same-frame loadFile).
|
|
217
|
+
const scheduledGeneration = generation;
|
|
218
|
+
queueMicrotask(() => {
|
|
219
|
+
if (state !== 'ready' || generation !== scheduledGeneration || !activePort)
|
|
220
|
+
return;
|
|
221
|
+
if (!readyHandlers.includes(entry))
|
|
222
|
+
return;
|
|
223
|
+
invokeReadyHandler(entry.handler);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
dispose() {
|
|
228
|
+
const i = readyHandlers.indexOf(entry);
|
|
229
|
+
if (i >= 0)
|
|
230
|
+
readyHandlers.splice(i, 1);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
},
|
|
156
234
|
send(channel, payload) {
|
|
157
235
|
if (!activePort)
|
|
158
236
|
return false;
|
|
@@ -160,9 +238,10 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
160
238
|
return true;
|
|
161
239
|
},
|
|
162
240
|
dispose() {
|
|
163
|
-
|
|
164
|
-
|
|
241
|
+
dropActivePort(true, 'absent');
|
|
242
|
+
state = 'disposed';
|
|
165
243
|
handlers.length = 0;
|
|
244
|
+
readyHandlers.length = 0;
|
|
166
245
|
},
|
|
167
246
|
};
|
|
168
247
|
}
|
|
@@ -112,6 +112,16 @@ export interface ViewManager {
|
|
|
112
112
|
* must trust its id — see `createWorkbenchSenderPolicy`.
|
|
113
113
|
*/
|
|
114
114
|
getHostToolbarWebContentsId(): number | null;
|
|
115
|
+
/**
|
|
116
|
+
* Return the last host-toolbar height NOTIFIED to the main-window renderer
|
|
117
|
+
* (an advertiser report in `'auto'` mode, a `setHeightMode({ fixed })` pin,
|
|
118
|
+
* or 0 after `hostToolbar.hide()`); 0 before any notify. The renderer pulls
|
|
119
|
+
* this on project-view mount to REPLAY a height whose push it missed — the
|
|
120
|
+
* toolbar's size-advertiser deduplicates and never re-reports, so a notify
|
|
121
|
+
* fired while no project view is mounted (cold start on the project list;
|
|
122
|
+
* always on close-project → reopen) would otherwise be lost forever.
|
|
123
|
+
*/
|
|
124
|
+
getHostToolbarHeight(): number;
|
|
115
125
|
/**
|
|
116
126
|
* NATIVE-HOST ONLY. Position the simulator content WebContentsView over the
|
|
117
127
|
* renderer-measured simulator panel REGION rect (the flex:1 placeholder slot,
|
|
@@ -168,10 +178,12 @@ export interface ViewManager {
|
|
|
168
178
|
}): void;
|
|
169
179
|
/**
|
|
170
180
|
* Reverse size-advertiser sink: the toolbar WCV's own renderer advertises
|
|
171
|
-
* its intrinsic content height (block-axis extent); we
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
181
|
+
* its intrinsic content height (block-axis extent); we retain it as the
|
|
182
|
+
* last-notified height (`getHostToolbarHeight`) and push it to the
|
|
183
|
+
* main-window renderer so the placeholder div resizes (closing the
|
|
184
|
+
* dynamic-height loop). Ignored ENTIRELY while a `{ fixed }` height mode is
|
|
185
|
+
* pinned via `hostToolbar.setHeightMode` — dropped reports neither notify
|
|
186
|
+
* nor touch the retained value (the session-resident advertiser always
|
|
175
187
|
* runs, so its reports must not fight a host-pinned height).
|
|
176
188
|
*/
|
|
177
189
|
setHostToolbarHeight(extent: number): void;
|
|
@@ -224,6 +236,18 @@ export interface HostToolbarControl {
|
|
|
224
236
|
* (idempotent).
|
|
225
237
|
*/
|
|
226
238
|
onMessage(channel: string, handler: (payload: unknown) => void): HostToolbarMessageSubscription;
|
|
239
|
+
/**
|
|
240
|
+
* Observe handshake readiness — the push counterpart to polling `send()`
|
|
241
|
+
* for `true`. Fires the handler once per load generation, exactly when
|
|
242
|
+
* that load's MessagePort handshake completes; registering while the
|
|
243
|
+
* channel is ALREADY ready fires once asynchronously on a microtask
|
|
244
|
+
* (missed-signal race guard, re-validated at fire time). A reload /
|
|
245
|
+
* re-handshake fires registered handlers again; a host-initiated
|
|
246
|
+
* `loadURL`/`loadFile` invalidates readiness at initiation, so handlers
|
|
247
|
+
* registered in that window wait for the NEW document's handshake.
|
|
248
|
+
* `dispose()` detaches (idempotent); `disposeAll` sweeps everything.
|
|
249
|
+
*/
|
|
250
|
+
onReady(handler: () => void): HostToolbarMessageSubscription;
|
|
227
251
|
/**
|
|
228
252
|
* Post `{ channel, payload }` to the toolbar page (received via
|
|
229
253
|
* `window.diminaHostToolbar.onMessage(channel, handler)`). Gated and
|
|
@@ -95,6 +95,13 @@ export function createViewManager(ctx) {
|
|
|
95
95
|
// Placeholder height authority: 'auto' = advertiser reports forward to the
|
|
96
96
|
// renderer; { fixed } = host-pinned, advertiser reports are dropped.
|
|
97
97
|
let hostToolbarHeightMode = 'auto';
|
|
98
|
+
// Last toolbar height NOTIFIED to the main-window renderer — the replay
|
|
99
|
+
// source behind `getHostToolbarHeight()`. Updated ONLY inside
|
|
100
|
+
// `notifyHostToolbarHeight` so the retained value can never diverge from
|
|
101
|
+
// what the renderer was told (an advertiser report dropped by a `{ fixed }`
|
|
102
|
+
// pin must not pollute it, and a setHeightMode validation reject leaves it
|
|
103
|
+
// untouched).
|
|
104
|
+
let hostToolbarLastHeight = 0;
|
|
98
105
|
// Gated narrow channel to the toolbar PAGE (per-load MessagePort handshake;
|
|
99
106
|
// see host-toolbar-port-channel.ts). Control-level registry — created with
|
|
100
107
|
// the manager so onMessage() works before any toolbar view exists.
|
|
@@ -248,18 +255,30 @@ export function createViewManager(ctx) {
|
|
|
248
255
|
}
|
|
249
256
|
view.setBounds(bounds);
|
|
250
257
|
}
|
|
258
|
+
// Single funnel for the height notify: retain-then-push, so the retained
|
|
259
|
+
// value is exactly the last value the renderer was told. Every height
|
|
260
|
+
// notify site MUST go through here — the renderer pulls the retained value
|
|
261
|
+
// on project-view mount to replay a push it missed (the toolbar's
|
|
262
|
+
// size-advertiser deduplicates and never re-reports).
|
|
263
|
+
function notifyHostToolbarHeight(height) {
|
|
264
|
+
hostToolbarLastHeight = height;
|
|
265
|
+
ctx.notify.hostToolbarHeightChanged(height);
|
|
266
|
+
}
|
|
251
267
|
function setHostToolbarHeight(extent) {
|
|
252
268
|
// While the host pins a fixed height, drop advertiser reports entirely —
|
|
253
269
|
// the session-resident advertiser is always installed, so forwarding its
|
|
254
270
|
// reports would make the strip oscillate between the pinned and measured
|
|
255
|
-
// heights on every content resize.
|
|
271
|
+
// heights on every content resize. Dropped reports must not touch the
|
|
272
|
+
// retained value either: retention records what was NOTIFIED, not what
|
|
273
|
+
// was reported.
|
|
256
274
|
if (hostToolbarHeightMode !== 'auto')
|
|
257
275
|
return;
|
|
258
276
|
// Push the reserved height back to the main-window renderer so its
|
|
259
|
-
// placeholder div resizes (closing the dynamic-height loop). The
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
277
|
+
// placeholder div resizes (closing the dynamic-height loop). The notified
|
|
278
|
+
// height IS retained in main (`getHostToolbarHeight`) so a renderer that
|
|
279
|
+
// mounts later can pull/replay it; the renderer placeholder remains the
|
|
280
|
+
// geometry authority — the forward anchor re-reports bounds from it.
|
|
281
|
+
notifyHostToolbarHeight(extent);
|
|
263
282
|
}
|
|
264
283
|
function hideHostToolbar() {
|
|
265
284
|
if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
|
|
@@ -273,7 +292,9 @@ export function createViewManager(ctx) {
|
|
|
273
292
|
// non-zero reserved height and re-publishes bounds on the next window
|
|
274
293
|
// resize, silently re-adding the view we just hid (unstable hide). Zeroing
|
|
275
294
|
// the height flips the anchor to `present:false` so it stops re-publishing.
|
|
276
|
-
|
|
295
|
+
// Through the funnel so the retained value follows to 0 — a renderer
|
|
296
|
+
// mounting after the hide must replay 0, not the stale pre-hide height.
|
|
297
|
+
notifyHostToolbarHeight(0);
|
|
277
298
|
}
|
|
278
299
|
const hostToolbar = {
|
|
279
300
|
async loadURL(url) {
|
|
@@ -306,19 +327,31 @@ export function createViewManager(ctx) {
|
|
|
306
327
|
hostToolbarPreloadOverride = path;
|
|
307
328
|
},
|
|
308
329
|
setHeightMode(mode) {
|
|
330
|
+
// Validate BEFORE touching any state: a poisoned `{ fixed }` (NaN /
|
|
331
|
+
// ±Infinity / negative) must neither reach the renderer placeholder
|
|
332
|
+
// (`height: NaNpx` corrupts the strip with no error anywhere) nor
|
|
333
|
+
// clobber the standing mode — fail-closed, not fail-corrupt.
|
|
334
|
+
if (mode !== 'auto' && !(Number.isFinite(mode.fixed) && mode.fixed >= 0)) {
|
|
335
|
+
throw new TypeError(`hostToolbar.setHeightMode: fixed height must be a finite, non-negative number (got ${mode.fixed})`);
|
|
336
|
+
}
|
|
309
337
|
hostToolbarHeightMode = mode;
|
|
310
338
|
if (mode !== 'auto') {
|
|
311
339
|
// Pin immediately: a preload-less/static toolbar never advertises, so
|
|
312
340
|
// waiting for the next report would leave the strip at height 0.
|
|
313
|
-
|
|
341
|
+
notifyHostToolbarHeight(mode.fixed);
|
|
314
342
|
}
|
|
315
343
|
// Switching back to 'auto' deliberately does NOT synthesize a notify —
|
|
316
344
|
// replaying a stale cached height would flash the old size; the NEXT
|
|
317
|
-
// advertiser report drives the placeholder again.
|
|
345
|
+
// advertiser report drives the placeholder again. The RETAINED value
|
|
346
|
+
// survives the switch though: a freshly-mounting renderer still needs
|
|
347
|
+
// the pinned height until that next report lands.
|
|
318
348
|
},
|
|
319
349
|
onMessage(channel, handler) {
|
|
320
350
|
return hostToolbarPort.onMessage(channel, handler);
|
|
321
351
|
},
|
|
352
|
+
onReady(handler) {
|
|
353
|
+
return hostToolbarPort.onReady(handler);
|
|
354
|
+
},
|
|
322
355
|
send(channel, payload) {
|
|
323
356
|
return hostToolbarPort.send(channel, payload);
|
|
324
357
|
},
|
|
@@ -1136,6 +1169,7 @@ export function createViewManager(ctx) {
|
|
|
1136
1169
|
return popoverView.webContents.id;
|
|
1137
1170
|
},
|
|
1138
1171
|
getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
|
|
1172
|
+
getHostToolbarHeight: () => hostToolbarLastHeight,
|
|
1139
1173
|
setNativeSimulatorViewBounds,
|
|
1140
1174
|
resize,
|
|
1141
1175
|
setSimulatorDevtoolsBounds,
|