@dimina-kit/devtools 0.4.0-dev.20260701083540 → 0.4.0-dev.20260702145840
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/dist/main/app/app.js +11 -0
- package/dist/main/app/max-listeners-diagnostic.d.ts +32 -0
- package/dist/main/app/max-listeners-diagnostic.js +63 -0
- package/dist/main/index.bundle.js +1657 -1158
- package/dist/main/ipc/bridge-router.d.ts +33 -4
- package/dist/main/ipc/bridge-router.js +200 -51
- package/dist/main/ipc/session-listener-bag.d.ts +41 -0
- package/dist/main/ipc/session-listener-bag.js +40 -0
- package/dist/main/ipc/simulator.js +5 -5
- package/dist/main/ipc/views.js +8 -19
- package/dist/main/services/automation/wait-active-page.d.ts +4 -2
- package/dist/main/services/elements-forward/index.js +19 -0
- package/dist/main/services/network-forward/index.js +13 -0
- package/dist/main/services/render-inspect/index.d.ts +9 -0
- package/dist/main/services/render-inspect/index.js +13 -1
- package/dist/main/services/simulator-storage/index.d.ts +9 -0
- package/dist/main/services/simulator-storage/index.js +26 -1
- package/dist/main/services/simulator-wxml/index.js +117 -11
- package/dist/main/services/views/apply-view-ops.d.ts +11 -0
- package/dist/main/services/views/apply-view-ops.js +22 -0
- package/dist/main/services/views/destroy-child-view.d.ts +9 -0
- package/dist/main/services/views/destroy-child-view.js +23 -0
- package/dist/main/services/views/host-toolbar-view.d.ts +21 -0
- package/dist/main/services/views/host-toolbar-view.js +249 -0
- package/dist/main/services/views/inject-when-ready.d.ts +41 -0
- package/dist/main/services/views/inject-when-ready.js +62 -0
- package/dist/main/services/views/native-simulator-devtools-host.d.ts +26 -0
- package/dist/main/services/views/native-simulator-devtools-host.js +484 -0
- package/dist/main/services/views/native-simulator-view.d.ts +28 -0
- package/dist/main/services/views/native-simulator-view.js +428 -0
- package/dist/main/services/views/overlay-panels-view.d.ts +29 -0
- package/dist/main/services/views/overlay-panels-view.js +150 -0
- package/dist/main/services/views/placement-reconciler.d.ts +73 -0
- package/dist/main/services/views/placement-reconciler.js +120 -0
- package/dist/main/services/views/placement-test-driver.d.ts +25 -0
- package/dist/main/services/views/placement-test-driver.js +48 -0
- package/dist/main/services/views/resolve-project-editor-target.d.ts +13 -0
- package/dist/main/services/views/resolve-project-editor-target.js +33 -0
- package/dist/main/services/views/view-manager.d.ts +28 -70
- package/dist/main/services/views/view-manager.js +66 -1514
- package/dist/main/services/views/workbench-view.d.ts +15 -0
- package/dist/main/services/views/workbench-view.js +170 -0
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/preload/windows/host-toolbar-runtime.cjs +6 -13
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs +10 -1
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +6 -3
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +6 -3
- package/dist/render-host/render-inspect.js +35 -1
- package/dist/renderer/assets/index-ChF9V4r-.js +49 -0
- package/dist/renderer/assets/{input-DR7bfyQE.js → input-Cjk0wSEh.js} +2 -2
- package/dist/renderer/assets/ipc-transport-D5dKIti1.css +1 -0
- package/dist/renderer/assets/{ipc-transport-3Arg8jPC.js → ipc-transport-OZS-KmOW.js} +2 -2
- package/dist/renderer/assets/{popover-CPZX3MrT.js → popover-CQVvOe90.js} +2 -2
- package/dist/renderer/assets/{select-BxyPlVyJ.js → select-BQDXi5ih.js} +2 -2
- package/dist/renderer/assets/{settings-api-CtPPJF-F.js → settings-api-B6x2mhaD.js} +2 -2
- package/dist/renderer/assets/{settings-CG_OR03F.js → settings-tP3Px2KR.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-ClqCBGOa.js → workbenchSettings-Bim9ol9R.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/service-host/sync-api-patch.js +33 -3
- package/dist/shared/bridge-channels.d.ts +17 -0
- package/dist/shared/bridge-channels.js +9 -0
- package/dist/shared/ipc-channels.d.ts +29 -14
- package/dist/shared/ipc-channels.js +16 -17
- package/dist/shared/ipc-schemas.d.ts +25 -17
- package/dist/shared/ipc-schemas.js +38 -22
- package/dist/shared/view-ids.d.ts +19 -0
- package/dist/shared/view-ids.js +25 -0
- package/dist/simulator/assets/{bridge-channels-BUQ5AbvJ.js → bridge-channels-zhsumfky.js} +2 -2
- package/dist/simulator/assets/{device-shell-CT3LRnTB.js → device-shell-CUl0ILfn.js} +2 -2
- package/dist/simulator/assets/{simulator-BizhGGE3.js → simulator-Dz8iGcgy.js} +5 -5
- package/dist/simulator/assets/{simulator-mini-app-BfNuDF3R.js → simulator-mini-app-Bt639XL_.js} +2 -2
- package/dist/simulator/simulator.html +2 -1
- package/package.json +4 -4
- package/dist/renderer/assets/index-HE9Xr9bQ.js +0 -49
- package/dist/renderer/assets/ipc-transport-Dt0jR7Ir.css +0 -1
|
@@ -12,18 +12,42 @@ import type { DebugTap } from '@dimina-kit/electron-deck/main';
|
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Render-side activity worth re-reading panel data on: a page's DOM mounted
|
|
15
|
-
* (`domReady`)
|
|
16
|
-
* from the
|
|
17
|
-
*
|
|
15
|
+
* (`domReady`), the visible page changed (`activePage`), or the active page's
|
|
16
|
+
* DOM mutated in place (`domMutated`, from the render-guest MutationObserver).
|
|
17
|
+
* Panels that pull from the active render guest (WXML/element-inspect) subscribe
|
|
18
|
+
* via `BridgeRouterHandle.onRenderEvent` so they can refresh without polling.
|
|
18
19
|
*/
|
|
19
20
|
export interface RenderEvent {
|
|
20
|
-
kind: 'domReady' | 'activePage';
|
|
21
|
+
kind: 'domReady' | 'activePage' | 'domMutated';
|
|
21
22
|
appId: string;
|
|
22
23
|
bridgeId: string;
|
|
23
24
|
/** activePage only: the now-visible page's route (bare pagePath), if known.
|
|
24
25
|
* Lets the current-page panel push the route without a separate lookup. */
|
|
25
26
|
pagePath?: string;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Point-in-time counts of every resource class RouterState owns. Leak coverage
|
|
30
|
+
* (unit + e2e) asserts EXACT equality of this ledger around a churn cycle —
|
|
31
|
+
* coarse memory sampling cannot see a leaked listener or a stale map entry, so
|
|
32
|
+
* the owner of the bookkeeping reports precise counts instead.
|
|
33
|
+
*/
|
|
34
|
+
export interface BridgeResourceCensus {
|
|
35
|
+
appSessions: number;
|
|
36
|
+
pageSessions: number;
|
|
37
|
+
serviceWcBindings: number;
|
|
38
|
+
/** Distinct live simulator webContents currently hosting ≥1 session. */
|
|
39
|
+
simulatorWcs: number;
|
|
40
|
+
/** Total session memberships across all simulator wcs. */
|
|
41
|
+
simulatorWcBindings: number;
|
|
42
|
+
renderWcBindings: number;
|
|
43
|
+
pendingApiCalls: number;
|
|
44
|
+
/**
|
|
45
|
+
* `listenerCount('destroyed')` per unique live simulator wc (keyed by wc id).
|
|
46
|
+
* One teardown hook per live session — a count above the hosted-session count
|
|
47
|
+
* is the one-dead-listener-per-soft-reload leak class.
|
|
48
|
+
*/
|
|
49
|
+
simulatorDestroyedListeners: Record<number, number>;
|
|
50
|
+
}
|
|
27
51
|
export interface BridgeRouterHandle {
|
|
28
52
|
/** Whether native-host mode is on (main is the source of truth). */
|
|
29
53
|
isNativeHost(): boolean;
|
|
@@ -70,6 +94,11 @@ export interface BridgeRouterHandle {
|
|
|
70
94
|
* error channel.
|
|
71
95
|
*/
|
|
72
96
|
disposeSessionsForSimulator?(simulatorWcId: number): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* The router's resource ledger (see BridgeResourceCensus). Optional so the
|
|
99
|
+
* many partial BridgeRouterHandle test mocks don't each have to stub it.
|
|
100
|
+
*/
|
|
101
|
+
census?(): BridgeResourceCensus;
|
|
73
102
|
/**
|
|
74
103
|
* The flag-gated debugTap (§7) over the bridge message stream. Exposed so a
|
|
75
104
|
* hidden devtools panel / automation can read `.entries()` when
|
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import { BRIDGE_CHANNELS as C, SIMULATOR_EVENTS as E, deviceInfoToHostEnv } from '../../shared/bridge-channels.js';
|
|
5
5
|
import { isPersistentSimulatorApi } from '../../shared/simulator-api-metadata.js';
|
|
6
6
|
import { devtoolsPackageRoot } from '../utils/paths.js';
|
|
7
|
+
import { createSessionListenerBag } from './session-listener-bag.js';
|
|
7
8
|
import { createDebugTap } from '@dimina-kit/electron-deck/main';
|
|
8
9
|
import { startDiminaResourceServer } from '../services/dimina-resource-server.js';
|
|
9
10
|
import { buildServiceHostSpawnUrl, createServiceHostWindow, navigateServiceHost, serviceHostSpec, } from '../windows/service-host-window/create.js';
|
|
@@ -147,7 +148,8 @@ export function installBridgeRouter(ctx) {
|
|
|
147
148
|
const state = {
|
|
148
149
|
appSessions: new Map(),
|
|
149
150
|
pageSessions: new Map(),
|
|
150
|
-
|
|
151
|
+
serviceWcIdToAppSessionId: new Map(),
|
|
152
|
+
simulatorWcIdToAppSessionIds: new Map(),
|
|
151
153
|
wcIdToBridgeId: new Map(),
|
|
152
154
|
pendingApiCalls: new Map(),
|
|
153
155
|
pool: null,
|
|
@@ -299,8 +301,37 @@ export function installBridgeRouter(ctx) {
|
|
|
299
301
|
return Promise.all(ids.map((id) => disposeAppSession(state, id))).then(() => { });
|
|
300
302
|
},
|
|
301
303
|
debugTap: state.debugTap,
|
|
304
|
+
census: () => {
|
|
305
|
+
const simulatorDestroyedListeners = {};
|
|
306
|
+
let simulatorWcBindings = 0;
|
|
307
|
+
for (const ids of state.simulatorWcIdToAppSessionIds.values())
|
|
308
|
+
simulatorWcBindings += ids.size;
|
|
309
|
+
for (const ap of state.appSessions.values()) {
|
|
310
|
+
const wc = ap.simulatorWc;
|
|
311
|
+
if (!wc.isDestroyed() && simulatorDestroyedListeners[wc.id] === undefined) {
|
|
312
|
+
simulatorDestroyedListeners[wc.id] = wc.listenerCount('destroyed');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
appSessions: state.appSessions.size,
|
|
317
|
+
pageSessions: state.pageSessions.size,
|
|
318
|
+
serviceWcBindings: state.serviceWcIdToAppSessionId.size,
|
|
319
|
+
simulatorWcs: state.simulatorWcIdToAppSessionIds.size,
|
|
320
|
+
simulatorWcBindings,
|
|
321
|
+
renderWcBindings: state.wcIdToBridgeId.size,
|
|
322
|
+
pendingApiCalls: state.pendingApiCalls.size,
|
|
323
|
+
simulatorDestroyedListeners,
|
|
324
|
+
};
|
|
325
|
+
},
|
|
302
326
|
};
|
|
303
327
|
ctx.bridge = bridgeHandle;
|
|
328
|
+
// e2e resource-census probe: Playwright's electronApp.evaluate() reads the
|
|
329
|
+
// router's ledger straight off this main-process global — no IPC surface
|
|
330
|
+
// needed. Test builds only.
|
|
331
|
+
if (process.env.NODE_ENV === 'test') {
|
|
332
|
+
;
|
|
333
|
+
globalThis.__diminaResourceCensus = () => bridgeHandle.census?.();
|
|
334
|
+
}
|
|
304
335
|
// Always-on guest console fan-out. Owns `ctx.guestConsole` (the sink the
|
|
305
336
|
// consoleLog case below routes to) so that render-layer console output is
|
|
306
337
|
// mirrored into the service host's own console — surfacing it in the embedded
|
|
@@ -322,8 +353,7 @@ export function installBridgeRouter(ctx) {
|
|
|
322
353
|
const ap = state.appSessions.get(payload.appSessionId);
|
|
323
354
|
if (!ap)
|
|
324
355
|
return;
|
|
325
|
-
|
|
326
|
-
if (!senderApp || senderApp.appSessionId !== ap.appSessionId)
|
|
356
|
+
if (!senderBoundToSession(state, event.sender, ap))
|
|
327
357
|
return;
|
|
328
358
|
if (ap.pages.has(payload.bridgeId)) {
|
|
329
359
|
ap.activeBridgeId = payload.bridgeId;
|
|
@@ -340,8 +370,7 @@ export function installBridgeRouter(ctx) {
|
|
|
340
370
|
const ap = state.appSessions.get(payload.appSessionId);
|
|
341
371
|
if (!ap)
|
|
342
372
|
return;
|
|
343
|
-
|
|
344
|
-
if (!senderApp || senderApp.appSessionId !== ap.appSessionId)
|
|
373
|
+
if (!senderBoundToSession(state, event.sender, ap))
|
|
345
374
|
return;
|
|
346
375
|
ap.pageStack = payload.stack;
|
|
347
376
|
};
|
|
@@ -366,8 +395,7 @@ export function installBridgeRouter(ctx) {
|
|
|
366
395
|
// its tab (mirrors the default path's postMessage(pageUnload) eviction).
|
|
367
396
|
if (payload.event === 'pageUnload' && ctx.appData) {
|
|
368
397
|
const ap = state.appSessions.get(payload.appSessionId);
|
|
369
|
-
|
|
370
|
-
if (ap && senderApp && senderApp.appSessionId === ap.appSessionId) {
|
|
398
|
+
if (ap && senderBoundToSession(state, event.sender, ap)) {
|
|
371
399
|
ctx.appData.evictBridge(ap.appId, payload.bridgeId);
|
|
372
400
|
}
|
|
373
401
|
}
|
|
@@ -384,9 +412,11 @@ export function installBridgeRouter(ctx) {
|
|
|
384
412
|
if (!target)
|
|
385
413
|
return;
|
|
386
414
|
// Only the app's service window or simulator window can issue dispose.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
415
|
+
// (Unknown senders pass — lenient by design; a simulator WCV is authorized
|
|
416
|
+
// for every session it hosts, so an older session's own dispose stays
|
|
417
|
+
// valid while a newer spawn shares the wc.)
|
|
418
|
+
if (!senderBoundToSession(state, event.sender, target) && appByWc(state, event.sender)) {
|
|
419
|
+
console.warn(`[bridge-router] DISPOSE rejected: sender not bound to target ${target.appSessionId}`);
|
|
390
420
|
return;
|
|
391
421
|
}
|
|
392
422
|
// AppData bridge eviction happens inside disposeAppSession (single
|
|
@@ -581,8 +611,9 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
581
611
|
pages: new Map(),
|
|
582
612
|
activeBridgeId: null,
|
|
583
613
|
poolEntryId,
|
|
584
|
-
onServiceClosed: null,
|
|
585
614
|
onServiceBoot: null,
|
|
615
|
+
listenerBag: createSessionListenerBag(),
|
|
616
|
+
registryHandle: null,
|
|
586
617
|
};
|
|
587
618
|
const rootPage = {
|
|
588
619
|
bridgeId,
|
|
@@ -600,8 +631,8 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
600
631
|
state.appSessions.set(appSessionId, appSession);
|
|
601
632
|
state.pageSessions.set(bridgeId, rootPage);
|
|
602
633
|
appSession.pages.set(bridgeId, rootPage);
|
|
603
|
-
bindWc(state.
|
|
604
|
-
|
|
634
|
+
bindWc(state.serviceWcIdToAppSessionId, serviceWindow.webContents, appSessionId);
|
|
635
|
+
bindSimulatorWc(state.simulatorWcIdToAppSessionIds, simulatorWc, appSessionId);
|
|
605
636
|
// Track the service-host webContents as a Connection (foundation.md §4.3) and
|
|
606
637
|
// own this session's serviceWc→appSessionId binding on its CURRENT lifetime
|
|
607
638
|
// segment. On a pooled-window REUSE, `disposeAppSession` calls
|
|
@@ -613,16 +644,15 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
613
644
|
// wc outlives individual app sessions — cross-connection state, §4.4 — so it
|
|
614
645
|
// cannot hang off any single session's segment.)
|
|
615
646
|
state.connections.acquire(serviceWindow.webContents).own(() => {
|
|
616
|
-
if (state.
|
|
617
|
-
state.
|
|
647
|
+
if (state.serviceWcIdToAppSessionId.get(appSession.serviceWc.id) === appSessionId) {
|
|
648
|
+
state.serviceWcIdToAppSessionId.delete(appSession.serviceWc.id);
|
|
618
649
|
}
|
|
619
650
|
});
|
|
620
|
-
ctx.registry.add(() => disposeAppSession(state, appSessionId));
|
|
651
|
+
appSession.registryHandle = ctx.registry.add(() => disposeAppSession(state, appSessionId));
|
|
621
652
|
const onServiceClosed = () => {
|
|
622
653
|
void disposeAppSession(state, appSessionId, { serviceAlreadyClosed: true });
|
|
623
654
|
};
|
|
624
|
-
appSession.
|
|
625
|
-
serviceWindow.once('closed', onServiceClosed);
|
|
655
|
+
appSession.listenerBag.once(serviceWindow, 'closed', onServiceClosed);
|
|
626
656
|
// The simulator WCV owns the app's UI lifetime. When it is destroyed —
|
|
627
657
|
// project close (`views.disposeAll`/detach) or a DeviceShell respawn
|
|
628
658
|
// (`attachNativeSimulator`, e.g. watcher hot reload) — the guest never gets
|
|
@@ -635,7 +665,7 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
635
665
|
const onSimulatorDestroyed = () => {
|
|
636
666
|
void disposeAppSession(state, appSessionId);
|
|
637
667
|
};
|
|
638
|
-
|
|
668
|
+
appSession.listenerBag.once(simulatorWc, 'destroyed', onSimulatorDestroyed);
|
|
639
669
|
// The simulator WCV may have been torn down DURING this spawn's awaits
|
|
640
670
|
// (loadAppConfig / resource server / service-host creation) — e.g. a project
|
|
641
671
|
// close/switch that ran while we were mid-flight. In that case its 'destroyed'
|
|
@@ -702,8 +732,7 @@ async function handlePageOpen(state, event, opts) {
|
|
|
702
732
|
if (!ap)
|
|
703
733
|
throw new Error(`[bridge-router] PAGE_OPEN unknown appSession ${opts.appSessionId}`);
|
|
704
734
|
// Only the simulator window owning the app can open additional pages.
|
|
705
|
-
|
|
706
|
-
if (!senderApp || senderApp.appSessionId !== opts.appSessionId) {
|
|
735
|
+
if (!senderBoundToSession(state, event.sender, ap)) {
|
|
707
736
|
throw new Error('[bridge-router] PAGE_OPEN rejected: caller not bound to app session');
|
|
708
737
|
}
|
|
709
738
|
const pagePath = normalizePagePath(opts.pagePath);
|
|
@@ -738,8 +767,7 @@ function handlePageClose(state, sender, payload) {
|
|
|
738
767
|
const ap = state.appSessions.get(page.appSessionId);
|
|
739
768
|
if (!ap)
|
|
740
769
|
return;
|
|
741
|
-
|
|
742
|
-
if (!senderApp || senderApp.appSessionId !== ap.appSessionId) {
|
|
770
|
+
if (!senderBoundToSession(state, sender, ap)) {
|
|
743
771
|
console.warn('[bridge-router] PAGE_CLOSE rejected: caller not bound to app session');
|
|
744
772
|
return;
|
|
745
773
|
}
|
|
@@ -749,8 +777,7 @@ function handlePageLifecycle(state, sender, payload) {
|
|
|
749
777
|
const ap = state.appSessions.get(payload.appSessionId);
|
|
750
778
|
if (!ap)
|
|
751
779
|
return;
|
|
752
|
-
|
|
753
|
-
if (!senderApp || senderApp.appSessionId !== ap.appSessionId)
|
|
780
|
+
if (!senderBoundToSession(state, sender, ap))
|
|
754
781
|
return;
|
|
755
782
|
forwardToService(ap, {
|
|
756
783
|
type: payload.event,
|
|
@@ -762,8 +789,7 @@ function handleNavCallback(state, sender, payload) {
|
|
|
762
789
|
const ap = state.appSessions.get(payload.appSessionId);
|
|
763
790
|
if (!ap)
|
|
764
791
|
return;
|
|
765
|
-
|
|
766
|
-
if (!senderApp || senderApp.appSessionId !== ap.appSessionId)
|
|
792
|
+
if (!senderBoundToSession(state, sender, ap))
|
|
767
793
|
return;
|
|
768
794
|
const result = { errMsg: payload.errMsg };
|
|
769
795
|
if (payload.ok) {
|
|
@@ -793,23 +819,50 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
793
819
|
reportLogicLoadFailure(ap, ctx);
|
|
794
820
|
return;
|
|
795
821
|
}
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
|
|
822
|
+
// A root page absent from the compiled manifest — most commonly a page the
|
|
823
|
+
// developer deleted, then hot-reloaded to — is not registered in logic.js.
|
|
824
|
+
// BOTH the service runtime AND the render runtime eager-`modRequire(pagePath)`
|
|
825
|
+
// it and throw `module <pagePath> not found`, which aborts the launch and
|
|
826
|
+
// leaves the simulator permanently blank. Gate BOTH sends and surface a
|
|
827
|
+
// WeChat-style diagnostic instead: the shell stays alive (an empty phone frame
|
|
828
|
+
// + a clear Console error) rather than a silent dead blank. Choosing a valid
|
|
829
|
+
// fallback page belongs at the renderer reload source (it owns the pagePath
|
|
830
|
+
// the render guest is spawned with); here we only refuse a load guaranteed to
|
|
831
|
+
// throw, so main and render never disagree about which page is live.
|
|
832
|
+
const rootPage = ap.pages.get(ap.appSessionId);
|
|
833
|
+
const rootMissing = !!rootPage && !pageInManifest(ap, rootPage.pagePath);
|
|
834
|
+
if (rootMissing) {
|
|
835
|
+
reportPageNotFound(ap, ctx, rootPage.pagePath);
|
|
836
|
+
}
|
|
837
|
+
else if (rootPage) {
|
|
838
|
+
// serviceLoaded is flipped only when service responds with
|
|
839
|
+
// `serviceResourceLoaded`; see handleContainerMsg. Setting it here would
|
|
840
|
+
// race a per-page `resourceLoaded` ahead of the service-side handler.
|
|
841
|
+
forwardToService(ap, makeLoadResource(ap, rootPage, 'service'));
|
|
842
|
+
}
|
|
800
843
|
// Flush any render `loadResource` that arrived (renderHostReady) while
|
|
801
844
|
// injection was in-flight — now that the bundle is confirmed present.
|
|
845
|
+
// `sendRenderLoadResource` refuses a page absent from the manifest, so a
|
|
846
|
+
// missing root never reaches the throwing render `modRequire`.
|
|
802
847
|
for (const page of ap.pages.values()) {
|
|
803
|
-
if (page.renderLoadPending)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
848
|
+
if (!page.renderLoadPending)
|
|
849
|
+
continue;
|
|
850
|
+
page.renderLoadPending = false;
|
|
851
|
+
sendRenderLoadResource(ap, page);
|
|
807
852
|
}
|
|
808
853
|
}
|
|
809
854
|
function sendRenderLoadResource(ap, page) {
|
|
810
|
-
if (page.renderWc
|
|
811
|
-
|
|
812
|
-
|
|
855
|
+
if (!page.renderWc || page.renderWc.isDestroyed())
|
|
856
|
+
return;
|
|
857
|
+
// Single choke point for BOTH the boot flush above and the late
|
|
858
|
+
// `renderHostReady` direct-send in `routeFromRender`: never send a render load
|
|
859
|
+
// for a page absent from the compiled manifest. The render runtime
|
|
860
|
+
// eager-`modRequire`s the pagePath and would throw `module <pagePath> not
|
|
861
|
+
// found`, blanking the simulator (a page the developer deleted, then
|
|
862
|
+
// hot-reloaded to). bootServiceHost surfaces the one-shot diagnostic.
|
|
863
|
+
if (!pageInManifest(ap, page.pagePath))
|
|
864
|
+
return;
|
|
865
|
+
page.renderWc.send(C.TO_RENDER, { msg: makeLoadResource(ap, page, 'render') });
|
|
813
866
|
}
|
|
814
867
|
/** The compiled logic.js URL `injectLogicBundle` fetches (mode-dependent). */
|
|
815
868
|
function logicBundleUrl(ap) {
|
|
@@ -856,6 +909,21 @@ function reportLogicLoadFailure(ap, ctx) {
|
|
|
856
909
|
console.error(message);
|
|
857
910
|
ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
|
|
858
911
|
}
|
|
912
|
+
/** Whether `pagePath` still exists in the app's freshly compiled manifest. */
|
|
913
|
+
function pageInManifest(ap, pagePath) {
|
|
914
|
+
return ap.manifest.pages.includes(normalizePagePath(pagePath));
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* WeChat-devtools-style "page does not exist" diagnostic. Emitted (main log +
|
|
918
|
+
* guest Console) when a mount targets a pagePath that is not in the compiled
|
|
919
|
+
* manifest — most commonly a page the developer deleted, then hot-reloaded.
|
|
920
|
+
*/
|
|
921
|
+
function reportPageNotFound(ap, ctx, pagePath) {
|
|
922
|
+
const message = `Page[${pagePath}] not found. May be caused by: 1. Forgetting to add page route in app.json. `
|
|
923
|
+
+ '2. Invoking Page() in async task.';
|
|
924
|
+
console.error(`[bridge-router] ${message}`);
|
|
925
|
+
ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
|
|
926
|
+
}
|
|
859
927
|
function maybeSendResourceLoaded(ap, page) {
|
|
860
928
|
if (page.resourceLoadedSent || !ap.serviceLoaded || !page.renderLoaded)
|
|
861
929
|
return;
|
|
@@ -977,6 +1045,20 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
|
977
1045
|
// Guarded + non-throwing: console capture must never break message routing.
|
|
978
1046
|
ctx.guestConsole?.emit(msg.body);
|
|
979
1047
|
break;
|
|
1048
|
+
case 'storageChanged':
|
|
1049
|
+
// Native-host SYNC storage liveness: the service-host's `setStorageSync`/etc.
|
|
1050
|
+
// write `localStorage` directly (no main round-trip), so they post the change
|
|
1051
|
+
// here for the Storage panel to stay live. Trust `ap.appId` (sender-resolved),
|
|
1052
|
+
// not any appId in the body. Guarded + non-throwing.
|
|
1053
|
+
ctx.onServiceStorageChanged?.(ap.appId, msg.body);
|
|
1054
|
+
break;
|
|
1055
|
+
case 'wxmlChanged':
|
|
1056
|
+
// Native-host WXML liveness: the render-guest MutationObserver posts this when
|
|
1057
|
+
// the active page's DOM mutated in place (setData). Surface it as a render
|
|
1058
|
+
// event so the WXML panel service re-pulls + pushes — same pipeline as
|
|
1059
|
+
// domReady/activePage. Trust `page.bridgeId` (sender-resolved), not the body.
|
|
1060
|
+
state.emitRenderEvent({ kind: 'domMutated', appId: ap.appId, bridgeId: page.bridgeId });
|
|
1061
|
+
break;
|
|
980
1062
|
default:
|
|
981
1063
|
break;
|
|
982
1064
|
}
|
|
@@ -1245,11 +1327,13 @@ function handleApiResponse(state, sender, payload) {
|
|
|
1245
1327
|
clearTimeout(pending.timer);
|
|
1246
1328
|
return;
|
|
1247
1329
|
}
|
|
1248
|
-
// Only
|
|
1330
|
+
// Only a simulator window bound to the OWNING session may respond. Membership
|
|
1331
|
+
// (senderBoundToSession), not sender-resolution equality: during a soft-reload
|
|
1332
|
+
// overlap the shared simulator wc still answers the outgoing session's
|
|
1333
|
+
// in-flight calls, which a latest-wins comparison would drop. Validate BEFORE
|
|
1249
1334
|
// mutating pending state so a spoofed/foreign response can't tear down a
|
|
1250
1335
|
// live subscription.
|
|
1251
|
-
|
|
1252
|
-
if (!senderApp || senderApp.appSessionId !== ap.appSessionId) {
|
|
1336
|
+
if (!senderBoundToSession(state, sender, ap)) {
|
|
1253
1337
|
console.warn('[bridge-router] API_RESPONSE rejected: sender not bound to app session');
|
|
1254
1338
|
return;
|
|
1255
1339
|
}
|
|
@@ -1380,9 +1464,20 @@ function pageFromMsg(state, ap, msg) {
|
|
|
1380
1464
|
function appByWc(state, wc) {
|
|
1381
1465
|
if (wc.isDestroyed())
|
|
1382
1466
|
return undefined;
|
|
1383
|
-
const appSessionId = state.
|
|
1467
|
+
const appSessionId = state.serviceWcIdToAppSessionId.get(wc.id);
|
|
1384
1468
|
if (appSessionId)
|
|
1385
1469
|
return state.appSessions.get(appSessionId);
|
|
1470
|
+
// A simulator wc hosts several sessions during a soft reload; a message that
|
|
1471
|
+
// doesn't name its session belongs to the LATEST still-alive spawn (the Set
|
|
1472
|
+
// preserves spawn order).
|
|
1473
|
+
const hosted = state.simulatorWcIdToAppSessionIds.get(wc.id);
|
|
1474
|
+
if (hosted) {
|
|
1475
|
+
let latest;
|
|
1476
|
+
for (const id of hosted)
|
|
1477
|
+
latest = state.appSessions.get(id) ?? latest;
|
|
1478
|
+
if (latest)
|
|
1479
|
+
return latest;
|
|
1480
|
+
}
|
|
1386
1481
|
const bridgeId = state.wcIdToBridgeId.get(wc.id);
|
|
1387
1482
|
if (bridgeId) {
|
|
1388
1483
|
const page = state.pageSessions.get(bridgeId);
|
|
@@ -1391,6 +1486,21 @@ function appByWc(state, wc) {
|
|
|
1391
1486
|
}
|
|
1392
1487
|
return undefined;
|
|
1393
1488
|
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Whether `sender` is authorized to control app session `ap`. A simulator WCV
|
|
1491
|
+
* is authorized for EVERY session it hosts (`simulatorWcIdToAppSessionIds` is
|
|
1492
|
+
* a membership set, so an older session's own control traffic stays valid
|
|
1493
|
+
* while a newer spawn boots on the same wc); any other sender (service host,
|
|
1494
|
+
* render guest) resolves one-to-one through `appByWc`.
|
|
1495
|
+
*/
|
|
1496
|
+
function senderBoundToSession(state, sender, ap) {
|
|
1497
|
+
if (sender.isDestroyed())
|
|
1498
|
+
return false;
|
|
1499
|
+
const hosted = state.simulatorWcIdToAppSessionIds.get(sender.id);
|
|
1500
|
+
if (hosted)
|
|
1501
|
+
return hosted.has(ap.appSessionId);
|
|
1502
|
+
return appByWc(state, sender)?.appSessionId === ap.appSessionId;
|
|
1503
|
+
}
|
|
1394
1504
|
function resolveAppByBridgeId(state, bridgeId) {
|
|
1395
1505
|
if (state.appSessions.has(bridgeId))
|
|
1396
1506
|
return state.appSessions.get(bridgeId);
|
|
@@ -1404,6 +1514,17 @@ function bindWc(map, wc, id) {
|
|
|
1404
1514
|
return;
|
|
1405
1515
|
map.set(wc.id, id);
|
|
1406
1516
|
}
|
|
1517
|
+
/** Add a session to a simulator wc's hosted set (created on first bind). */
|
|
1518
|
+
function bindSimulatorWc(map, wc, id) {
|
|
1519
|
+
if (wc.isDestroyed())
|
|
1520
|
+
return;
|
|
1521
|
+
let hosted = map.get(wc.id);
|
|
1522
|
+
if (!hosted) {
|
|
1523
|
+
hosted = new Set();
|
|
1524
|
+
map.set(wc.id, hosted);
|
|
1525
|
+
}
|
|
1526
|
+
hosted.add(id);
|
|
1527
|
+
}
|
|
1407
1528
|
function readBridgeId(msg) {
|
|
1408
1529
|
return typeof msg.body?.bridgeId === 'string' ? msg.body.bridgeId : undefined;
|
|
1409
1530
|
}
|
|
@@ -1428,6 +1549,15 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1428
1549
|
if (!ap)
|
|
1429
1550
|
return;
|
|
1430
1551
|
state.appSessions.delete(appSessionId);
|
|
1552
|
+
// Remove this session's shutdown-fallback registry entry so the registry does
|
|
1553
|
+
// not accumulate one stale closure per respawn. Ordered AFTER the delete
|
|
1554
|
+
// above: dispose() runs the wrapped fn (disposeAppSession again), which
|
|
1555
|
+
// early-returns now that the session is gone — no recursion, no double-run.
|
|
1556
|
+
// (At whole-app shutdown the registry marks the entry released before calling
|
|
1557
|
+
// the fn, so this dispose() is a no-op then.)
|
|
1558
|
+
const registryHandle = ap.registryHandle;
|
|
1559
|
+
ap.registryHandle = null;
|
|
1560
|
+
void registryHandle?.dispose();
|
|
1431
1561
|
state.appLifecycle.dispose(appSessionId);
|
|
1432
1562
|
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1433
1563
|
// page teardown below progressively empties (and finally clears).
|
|
@@ -1459,6 +1589,14 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1459
1589
|
state.pageSessions.delete(page.bridgeId);
|
|
1460
1590
|
}
|
|
1461
1591
|
ap.pages.clear();
|
|
1592
|
+
// Detach every session-scoped hook on emitters that outlive this session —
|
|
1593
|
+
// the shared simulator WCV's 'destroyed' and the service window's 'closed' —
|
|
1594
|
+
// BEFORE the window is released/closed below: a pool-recycled window must
|
|
1595
|
+
// not re-trigger this session's teardown from a stale 'closed', and the
|
|
1596
|
+
// simulator wc must not accumulate one dead hook per soft reload until the
|
|
1597
|
+
// MaxListeners warning fires. Idempotent with teardown being triggered BY
|
|
1598
|
+
// one of these hooks (removing a fired once() is a no-op).
|
|
1599
|
+
ap.listenerBag.dispose();
|
|
1462
1600
|
if (ap.poolEntryId !== null && state.pool && !opts.serviceAlreadyClosed) {
|
|
1463
1601
|
// Soft reuse (foundation.md §4.3): the pooled service-host webContents keeps
|
|
1464
1602
|
// its wc.id but is about to run a NEW app session. Reset its Connection
|
|
@@ -1468,13 +1606,10 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1468
1606
|
// clean segment. The connection object stays alive (it's reused), so this is
|
|
1469
1607
|
// reset (not close); a real destroy still closes it via its 'destroyed' hook.
|
|
1470
1608
|
state.connections.reset(ap.serviceWc.id);
|
|
1471
|
-
// Return the window to the pool instead of closing it. Detach the
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
// into the next spawn's recycled window.
|
|
1475
|
-
if (ap.onServiceClosed && !ap.serviceWindow.isDestroyed()) {
|
|
1476
|
-
ap.serviceWindow.removeListener('closed', ap.onServiceClosed);
|
|
1477
|
-
}
|
|
1609
|
+
// Return the window to the pool instead of closing it. Detach the boot
|
|
1610
|
+
// listener first (the 'closed' hook already came off with the listener-bag
|
|
1611
|
+
// dispose above) so a stale did-finish-load can't boot this disposed
|
|
1612
|
+
// session into the next spawn's recycled window.
|
|
1478
1613
|
if (ap.onServiceBoot && !ap.serviceWindow.isDestroyed()) {
|
|
1479
1614
|
ap.serviceWindow.webContents.removeListener('did-finish-load', ap.onServiceBoot);
|
|
1480
1615
|
}
|
|
@@ -1494,8 +1629,22 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1494
1629
|
else if (!opts.serviceAlreadyClosed && !ap.serviceWindow.isDestroyed()) {
|
|
1495
1630
|
ap.serviceWindow.close();
|
|
1496
1631
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1632
|
+
// Value-checked unbind for the service wc: this delete runs after the
|
|
1633
|
+
// pool-release await above, so on the pool path the next spawn may have
|
|
1634
|
+
// already re-acquired the SAME window and rebound its wc id — only remove
|
|
1635
|
+
// an entry that still names THIS session.
|
|
1636
|
+
if (state.serviceWcIdToAppSessionId.get(ap.serviceWc.id) === appSessionId) {
|
|
1637
|
+
state.serviceWcIdToAppSessionId.delete(ap.serviceWc.id);
|
|
1638
|
+
}
|
|
1639
|
+
// The simulator wc's hosted set sheds only this session; sessions that are
|
|
1640
|
+
// still live on the shared wc (the soft-reload survivor) keep their
|
|
1641
|
+
// membership. The map entry goes away with the last session.
|
|
1642
|
+
const hosted = state.simulatorWcIdToAppSessionIds.get(ap.simulatorWc.id);
|
|
1643
|
+
if (hosted) {
|
|
1644
|
+
hosted.delete(appSessionId);
|
|
1645
|
+
if (hosted.size === 0)
|
|
1646
|
+
state.simulatorWcIdToAppSessionIds.delete(ap.simulatorWc.id);
|
|
1647
|
+
}
|
|
1499
1648
|
// Only the local fallback server needs closing; the dev-server base is owned
|
|
1500
1649
|
// by the workspace session, not this app session.
|
|
1501
1650
|
if (ap.resourceServer) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped listener bookkeeping. An app session hangs lifecycle hooks on
|
|
3
|
+
* emitters it does NOT own — the shared simulator WebContentsView, a pooled
|
|
4
|
+
* service-host window — and those emitters outlive the session (one simulator
|
|
5
|
+
* wc soft-reloads through many sessions). Every such hook is registered
|
|
6
|
+
* through this bag so the session's single teardown chokepoint can detach them
|
|
7
|
+
* all with one `dispose()`; a hook left behind survives as a dead listener on
|
|
8
|
+
* the shared emitter, growing by one per soft reload until Node's MaxListeners
|
|
9
|
+
* warning fires.
|
|
10
|
+
*/
|
|
11
|
+
type Listener = (...args: unknown[]) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Structural surface of the Node/Electron emitters the bag manages.
|
|
14
|
+
* `removeListener(event, fn)` also detaches a `once(event, fn)` registration:
|
|
15
|
+
* Node's once wrapper keeps a `.listener` back-reference to `fn` and
|
|
16
|
+
* removeListener matches through it. `isDestroyed` covers Electron objects
|
|
17
|
+
* (WebContents / BrowserWindow) whose methods throw once destroyed.
|
|
18
|
+
*/
|
|
19
|
+
export interface BagEmitter {
|
|
20
|
+
on(event: string, listener: Listener): unknown;
|
|
21
|
+
once(event: string, listener: Listener): unknown;
|
|
22
|
+
removeListener(event: string, listener: Listener): unknown;
|
|
23
|
+
isDestroyed?(): boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface SessionListenerBag {
|
|
26
|
+
/** Attach `fn` via `emitter.on` and track it for dispose. No-op after dispose. */
|
|
27
|
+
on(emitter: BagEmitter, event: string, fn: Listener): void;
|
|
28
|
+
/** Attach `fn` via `emitter.once` and track it for dispose. No-op after dispose. */
|
|
29
|
+
once(emitter: BagEmitter, event: string, fn: Listener): void;
|
|
30
|
+
/**
|
|
31
|
+
* Detach every tracked listener that may still be attached. Emitters that
|
|
32
|
+
* report `isDestroyed()` are skipped — their listeners died with them, and
|
|
33
|
+
* touching a destroyed Electron object throws. Removing a `once` that
|
|
34
|
+
* already fired is a no-op, so dispose is safe to call FROM one of the
|
|
35
|
+
* bag's own hooks (teardown triggered by the hook itself). Idempotent.
|
|
36
|
+
*/
|
|
37
|
+
dispose(): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createSessionListenerBag(): SessionListenerBag;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=session-listener-bag.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped listener bookkeeping. An app session hangs lifecycle hooks on
|
|
3
|
+
* emitters it does NOT own — the shared simulator WebContentsView, a pooled
|
|
4
|
+
* service-host window — and those emitters outlive the session (one simulator
|
|
5
|
+
* wc soft-reloads through many sessions). Every such hook is registered
|
|
6
|
+
* through this bag so the session's single teardown chokepoint can detach them
|
|
7
|
+
* all with one `dispose()`; a hook left behind survives as a dead listener on
|
|
8
|
+
* the shared emitter, growing by one per soft reload until Node's MaxListeners
|
|
9
|
+
* warning fires.
|
|
10
|
+
*/
|
|
11
|
+
export function createSessionListenerBag() {
|
|
12
|
+
const records = [];
|
|
13
|
+
let disposed = false;
|
|
14
|
+
return {
|
|
15
|
+
on(emitter, event, fn) {
|
|
16
|
+
if (disposed)
|
|
17
|
+
return;
|
|
18
|
+
emitter.on(event, fn);
|
|
19
|
+
records.push({ emitter, event, fn });
|
|
20
|
+
},
|
|
21
|
+
once(emitter, event, fn) {
|
|
22
|
+
if (disposed)
|
|
23
|
+
return;
|
|
24
|
+
emitter.once(event, fn);
|
|
25
|
+
records.push({ emitter, event, fn });
|
|
26
|
+
},
|
|
27
|
+
dispose() {
|
|
28
|
+
if (disposed)
|
|
29
|
+
return;
|
|
30
|
+
disposed = true;
|
|
31
|
+
for (const { emitter, event, fn } of records) {
|
|
32
|
+
if (emitter.isDestroyed?.())
|
|
33
|
+
continue;
|
|
34
|
+
emitter.removeListener(event, fn);
|
|
35
|
+
}
|
|
36
|
+
records.length = 0;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=session-listener-bag.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ServiceHostChannel, SimulatorChannel, SimulatorCustomApiChannel } from '../../shared/ipc-channels.js';
|
|
2
|
-
import { SimulatorAttachNativeSchema, SimulatorCustomApiInvokeSchema, SimulatorSetDeviceInfoSchema,
|
|
2
|
+
import { SimulatorAttachNativeSchema, SimulatorCustomApiInvokeSchema, SimulatorSetDeviceInfoSchema, SimulatorSoftReloadSchema, } from '../../shared/ipc-schemas.js';
|
|
3
3
|
import { deviceInfoToHostEnv } from '../../shared/bridge-channels.js';
|
|
4
4
|
import { validate } from '../utils/ipc-schema.js';
|
|
5
5
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
@@ -8,13 +8,13 @@ export function registerSimulatorIpc(ctx) {
|
|
|
8
8
|
.handle(SimulatorChannel.AttachNative, (_, ...args) => {
|
|
9
9
|
const [simulatorUrl, simWidth] = validate(SimulatorChannel.AttachNative, SimulatorAttachNativeSchema, args);
|
|
10
10
|
return ctx.views.attachNativeSimulator(simulatorUrl, simWidth);
|
|
11
|
+
})
|
|
12
|
+
.handle(SimulatorChannel.SoftReload, (_, ...args) => {
|
|
13
|
+
const [simulatorUrl] = validate(SimulatorChannel.SoftReload, SimulatorSoftReloadSchema, args);
|
|
14
|
+
return ctx.views.softReloadNativeSimulator(simulatorUrl);
|
|
11
15
|
})
|
|
12
16
|
.handle(SimulatorChannel.Detach, () => {
|
|
13
17
|
ctx.views.detachSimulator();
|
|
14
|
-
})
|
|
15
|
-
.handle(SimulatorChannel.SetNativeBounds, (_, ...args) => {
|
|
16
|
-
const [p] = validate(SimulatorChannel.SetNativeBounds, SimulatorSetNativeBoundsSchema, args);
|
|
17
|
-
ctx.views.setNativeSimulatorViewBounds(p);
|
|
18
18
|
})
|
|
19
19
|
.handle(SimulatorChannel.SetDeviceInfo, (_, ...args) => {
|
|
20
20
|
const [device] = validate(SimulatorChannel.SetDeviceInfo, SimulatorSetDeviceInfoSchema, args);
|
package/dist/main/ipc/views.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ipcMain } from 'electron';
|
|
2
2
|
import { ViewChannel } from '../../shared/ipc-channels.js';
|
|
3
|
-
import {
|
|
3
|
+
import { PlacementSnapshotSchema, HostToolbarAdvertiseHeightSchema, } from '../../shared/ipc-schemas.js';
|
|
4
4
|
import { validate } from '../utils/ipc-schema.js';
|
|
5
5
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
6
6
|
/**
|
|
@@ -20,24 +20,13 @@ import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export function registerViewsIpc(ctx) {
|
|
22
22
|
const registry = new IpcRegistry(ctx.senderPolicy)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// was never attached (the host did not opt into the workbench editor).
|
|
31
|
-
.handle(ViewChannel.WorkbenchBounds, (_event, ...args) => {
|
|
32
|
-
const [bounds] = validate(ViewChannel.WorkbenchBounds, ViewBoundsSchema, args);
|
|
33
|
-
ctx.views.setWorkbenchBounds(bounds);
|
|
34
|
-
})
|
|
35
|
-
// Host-controllable toolbar: forward anchor (the MAIN renderer measures the
|
|
36
|
-
// placeholder rect → toolbar WCV bounds). invoke, mirroring
|
|
37
|
-
// SimulatorDevtoolsBounds. Sender is the trusted main renderer.
|
|
38
|
-
.handle(ViewChannel.HostToolbarBounds, (_event, ...args) => {
|
|
39
|
-
const [bounds] = validate(ViewChannel.HostToolbarBounds, ViewBoundsSchema, args);
|
|
40
|
-
ctx.views.setHostToolbarBounds(bounds);
|
|
23
|
+
// Window-level placement snapshot: the single source of truth for every
|
|
24
|
+
// managed native view's bounds/visibility/z-order. The renderer's central
|
|
25
|
+
// publisher coalesces one snapshot per frame; the reconciler diffs it
|
|
26
|
+
// against the actual view tree. Supersedes the per-view bounds channels.
|
|
27
|
+
.handle(ViewChannel.PlacementSnapshot, (_event, ...args) => {
|
|
28
|
+
const [snapshot] = validate(ViewChannel.PlacementSnapshot, PlacementSnapshotSchema, args);
|
|
29
|
+
ctx.views.setPlacementSnapshot(snapshot);
|
|
41
30
|
})
|
|
42
31
|
// Height replay pull: a freshly-mounted main-renderer placeholder asks for
|
|
43
32
|
// the last NOTIFIED toolbar height (main retains it — the toolbar's
|