@dimina-kit/electron-deck 0.1.0-dev.20260610082053

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/client/index.d.ts +20 -0
  4. package/dist/client/index.d.ts.map +1 -0
  5. package/dist/client/index.js +310 -0
  6. package/dist/client/index.js.map +1 -0
  7. package/dist/client/layout-client.d.ts +51 -0
  8. package/dist/client/layout-client.d.ts.map +1 -0
  9. package/dist/electron-deck-DfFKeFEC.js +2295 -0
  10. package/dist/electron-deck-DfFKeFEC.js.map +1 -0
  11. package/dist/electron-deck.d.ts +57 -0
  12. package/dist/electron-deck.d.ts.map +1 -0
  13. package/dist/errors-NE2ig7L5.js +53 -0
  14. package/dist/errors-NE2ig7L5.js.map +1 -0
  15. package/dist/errors.d.ts +33 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/events.d.ts +14 -0
  18. package/dist/events.d.ts.map +1 -0
  19. package/dist/host/capability.d.ts +50 -0
  20. package/dist/host/capability.d.ts.map +1 -0
  21. package/dist/host/control-bus.d.ts +92 -0
  22. package/dist/host/control-bus.d.ts.map +1 -0
  23. package/dist/host/index.d.ts +42 -0
  24. package/dist/host/index.d.ts.map +1 -0
  25. package/dist/host/index.js +2 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +3 -0
  29. package/dist/internal/deck-app.d.ts +415 -0
  30. package/dist/internal/deck-app.d.ts.map +1 -0
  31. package/dist/internal/electron-types.d.ts +131 -0
  32. package/dist/internal/electron-types.d.ts.map +1 -0
  33. package/dist/internal/event-bus.d.ts +29 -0
  34. package/dist/internal/event-bus.d.ts.map +1 -0
  35. package/dist/internal/ipc-registry-memory.d.ts +25 -0
  36. package/dist/internal/ipc-registry-memory.d.ts.map +1 -0
  37. package/dist/internal/lifecycle-manager.d.ts +28 -0
  38. package/dist/internal/lifecycle-manager.d.ts.map +1 -0
  39. package/dist/internal/resource-registry.d.ts +9 -0
  40. package/dist/internal/resource-registry.d.ts.map +1 -0
  41. package/dist/internal/trust-set.d.ts +53 -0
  42. package/dist/internal/trust-set.d.ts.map +1 -0
  43. package/dist/internal/wire-transport.d.ts +159 -0
  44. package/dist/internal/wire-transport.d.ts.map +1 -0
  45. package/dist/main/compositor.d.ts +123 -0
  46. package/dist/main/compositor.d.ts.map +1 -0
  47. package/dist/main/connection.d.ts +37 -0
  48. package/dist/main/connection.d.ts.map +1 -0
  49. package/dist/main/debug-tap.d.ts +55 -0
  50. package/dist/main/debug-tap.d.ts.map +1 -0
  51. package/dist/main/disposable.d.ts +18 -0
  52. package/dist/main/disposable.d.ts.map +1 -0
  53. package/dist/main/index.d.ts +16 -0
  54. package/dist/main/index.d.ts.map +1 -0
  55. package/dist/main/index.js +152 -0
  56. package/dist/main/index.js.map +1 -0
  57. package/dist/main/logger.d.ts +15 -0
  58. package/dist/main/logger.d.ts.map +1 -0
  59. package/dist/main/scope.d.ts +52 -0
  60. package/dist/main/scope.d.ts.map +1 -0
  61. package/dist/main/view-handle.d.ts +131 -0
  62. package/dist/main/view-handle.d.ts.map +1 -0
  63. package/dist/preload/index.cjs +105 -0
  64. package/dist/preload/index.cjs.map +1 -0
  65. package/dist/preload/index.d.ts +43 -0
  66. package/dist/preload/index.d.ts.map +1 -0
  67. package/dist/preload/index.js +80 -0
  68. package/dist/preload/index.js.map +1 -0
  69. package/dist/protocol-BJqo-vP2.js +23 -0
  70. package/dist/protocol-BJqo-vP2.js.map +1 -0
  71. package/dist/shared/protocol.d.ts +76 -0
  72. package/dist/shared/protocol.d.ts.map +1 -0
  73. package/dist/types.d.ts +504 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/view-handle-CR-yWNfr.js +701 -0
  76. package/dist/view-handle-CR-yWNfr.js.map +1 -0
  77. package/package.json +86 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EchoTechFE and dimina-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @dimina-kit/electron-deck
2
+
3
+ Dimina devtools 的 host 集成地基。提供:连接层(per-webContents 的 `Connection` + 资源归属)、host-shell 的跨进程 transport、`DeckConfig` 配置类型与 `defineEvent`,以及 webview 侧的 preload / client。
4
+
5
+ 下游 host(如 qdmp)写一份 `DeckConfig` 交给 `launch(config)`,framework 接管 Electron 装配、IPC、生命周期。
6
+
7
+ ## 入口包归属
8
+
9
+ `launch()` 入口函数住在 `@dimina-kit/devtools`(它要驱动 devtools 真运行时,而 devtools 已依赖本包——入口放本包会形成循环依赖)。本包提供配置类型、`defineEvent` 和 webview 侧工具:
10
+
11
+ | 你要的 | 从哪导入 |
12
+ |---|---|
13
+ | `launch(config)` 入口函数 | `@dimina-kit/devtools` |
14
+ | `defineEvent` / `DeckConfig` 等类型 | `@dimina-kit/electron-deck` |
15
+ | preload bridge `exposeDeckBridge()` | `@dimina-kit/electron-deck/preload` |
16
+ | renderer client `createDeckClient<HS, EV>()` | `@dimina-kit/electron-deck/client` |
17
+
18
+ ## 最小例子
19
+
20
+ ```ts
21
+ // main.ts
22
+ import { launch } from '@dimina-kit/devtools'
23
+ import { defineEvent } from '@dimina-kit/electron-deck'
24
+
25
+ const authChanged = defineEvent<{ user: { id: string } | null }>('authChanged')
26
+
27
+ // 不要顶层 `await launch(...)`:electron 在 main 模块求值完成前不触发
28
+ // app.whenReady(),而 launch() 内部要 await whenReady,顶层 await 会死锁。
29
+ launch({
30
+ app: { name: 'My DevTools' },
31
+ hostServices: { getUser: async () => ({ user: null }) },
32
+ events: [authChanged],
33
+ }).catch((err) => { console.error('launch() failed:', err) })
34
+ ```
35
+
36
+ ## 文档
37
+
38
+ - 连接层(Connection / 资源归属 / debugTap)参考:[`docs/foundation.md`](./docs/foundation.md)
39
+ - host 集成(config 字段 / Runtime / preload / client / 必知约束)参考:[`../devtools/docs/workbench-model.md`](../devtools/docs/workbench-model.md)
@@ -0,0 +1,20 @@
1
+ import { DeckClientNotReadyError, DeckRemoteError } from '../errors.js';
2
+ import { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL } from '../shared/protocol.js';
3
+ import type { DeckBridge } from '../shared/protocol.js';
4
+ import type { Disposable, HostEvent, JsonValue } from '../types.js';
5
+ export { DeckClientNotReadyError, DeckRemoteError };
6
+ export { createDeckLayoutClient } from './layout-client.js';
7
+ export type { SlotGrant, LayoutBridge, LayoutClientDeps, } from './layout-client.js';
8
+ export interface CreateDeckClientOptions {
9
+ /** 默认 `__electronDeckBridge`;必须与 host preload 对齐 */
10
+ readonly globalName?: string;
11
+ }
12
+ export interface DeckClient<HS extends Record<keyof HS, (...args: any[]) => unknown>, EV extends readonly HostEvent<JsonValue>[]> {
13
+ ready(): Promise<void>;
14
+ invoke<K extends keyof HS & string>(name: K, ...args: Parameters<HS[K]>): Promise<Awaited<ReturnType<HS[K]>>>;
15
+ on<E extends EV[number]>(event: E, listener: (payload: E extends HostEvent<infer P> ? P : never) => void): Disposable;
16
+ }
17
+ export declare function createDeckClient<HS extends Record<keyof HS, (...args: any[]) => unknown>, EV extends readonly HostEvent<JsonValue>[]>(options?: CreateDeckClientOptions): DeckClient<HS, EV>;
18
+ export declare function readBridgeFromGlobal(globalName?: string): DeckBridge | undefined;
19
+ export { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL };
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,uBAAuB,EACvB,eAAe,EACf,MAAM,cAAc,CAAA;AACrB,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,CAAA;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EACX,SAAS,EACT,YAAY,EACZ,gBAAgB,GAChB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,uBAAuB;IACvC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC5B;AASD,MAAM,WAAW,UAAU,CAE1B,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,EACxD,EAAE,SAAS,SAAS,SAAS,CAAC,SAAS,CAAC,EAAE;IAE1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,MAAM,EACjC,IAAI,EAAE,CAAC,EACP,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EACtB,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,KAAK,IAAI,GACnE,UAAU,CAAA;CACb;AAED,wBAAgB,gBAAgB,CAE/B,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,EACxD,EAAE,SAAS,SAAS,SAAS,CAAC,SAAS,CAAC,EAAE,EACzC,OAAO,CAAC,EAAE,uBAAuB,GAAG,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CA+DvD;AAED,wBAAgB,oBAAoB,CACnC,UAAU,GAAE,MAA8B,GACxC,UAAU,GAAG,SAAS,CAKxB;AASD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,CAAA"}
@@ -0,0 +1,310 @@
1
+ import { n as DeckRemoteError, t as DeckClientNotReadyError } from "../errors-NE2ig7L5.js";
2
+ import { n as DEFAULT_BRIDGE_GLOBAL, t as BRIDGE_PROTOCOL_VERSION } from "../protocol-BJqo-vP2.js";
3
+ //#region ../view-anchor/src/view-anchor.ts
4
+ var clampRect = (r) => ({
5
+ x: Math.round(r.x),
6
+ y: Math.round(r.y),
7
+ width: Math.max(0, Math.round(r.width)),
8
+ height: Math.max(0, Math.round(r.height))
9
+ });
10
+ /**
11
+ * Pure measure: read `target`'s rect and wrap it as an explicit visible
12
+ * Placement. Always `{ visible:true }` — hiddenness is a caller decision
13
+ * (see `createPlacementAnchor`), so this never returns `{ visible:false }`
14
+ * and never infers visibility from a 0 size. A collapsed (0×0) but present
15
+ * element therefore yields `{ visible:true, bounds:{...,width:0,height:0} }`,
16
+ * distinct from any hidden Placement.
17
+ */
18
+ function measurePlacement(target) {
19
+ const r = target.getBoundingClientRect();
20
+ return {
21
+ visible: true,
22
+ bounds: clampRect({
23
+ x: r.left,
24
+ y: r.top,
25
+ width: r.width,
26
+ height: r.height
27
+ })
28
+ };
29
+ }
30
+ var samePlacement = (a, b) => {
31
+ if (a.visible !== b.visible) return false;
32
+ if (a.visible && b.visible) return a.bounds.x === b.bounds.x && a.bounds.y === b.bounds.y && a.bounds.width === b.bounds.width && a.bounds.height === b.bounds.height;
33
+ return true;
34
+ };
35
+ /**
36
+ * The explicit-Placement mirror of `createViewAnchor`. Same observer/dedup/
37
+ * teardown machinery, but the sink receives a `Placement`:
38
+ * - `visible === true` → publish `measurePlacement(target)` and re-publish
39
+ * SYNCHRONOUSLY on every `ResizeObserver`/`resize` tick.
40
+ * - `visible === false` → publish `{ visible:false }` (NOT a ZERO bounds);
41
+ * do not observe.
42
+ *
43
+ * Dedup carries the discriminant (`samePlacement`), so a visibility flip is
44
+ * never coalesced away.
45
+ *
46
+ * Opt-in `guardDisplayNone` (default false): when on, a measured zero-area
47
+ * target (display:none / unmounted / unstable first layout) publishes
48
+ * `{ visible:false }` instead of `{ visible:true, bounds:0×0 }`, and an
49
+ * IntersectionObserver is attached so a display:none transition (which
50
+ * ResizeObserver does not report) re-publishes.
51
+ */
52
+ function createPlacementAnchor(target, opts) {
53
+ let visible = opts.visible;
54
+ let publish = opts.publish;
55
+ const guardDisplayNone = opts.guardDisplayNone ?? false;
56
+ const followScroll = opts.followScroll ?? false;
57
+ const followGeometry = opts.followGeometry ?? false;
58
+ let observer = null;
59
+ let io = null;
60
+ let lastPublished = null;
61
+ let disposed = false;
62
+ let rafId = null;
63
+ let steadyFrames = 0;
64
+ const STEADY_CLOSE_FRAMES = 2;
65
+ let pointerHeld = false;
66
+ let sentinelDeadline = null;
67
+ const computePlacement = () => {
68
+ const p = measurePlacement(target);
69
+ if (guardDisplayNone && p.visible && (p.bounds.width === 0 || p.bounds.height === 0)) return { visible: false };
70
+ return p;
71
+ };
72
+ const emit = () => {
73
+ if (disposed || !visible) return;
74
+ const p = computePlacement();
75
+ if (lastPublished && samePlacement(lastPublished, p)) return;
76
+ lastPublished = p;
77
+ publish(p);
78
+ };
79
+ const sentinelFrame = () => {
80
+ rafId = null;
81
+ if (disposed || !visible) {
82
+ sentinelDeadline = null;
83
+ return;
84
+ }
85
+ if (sentinelDeadline !== null && performance.now() >= sentinelDeadline) {
86
+ sentinelDeadline = null;
87
+ return;
88
+ }
89
+ const p = computePlacement();
90
+ if (lastPublished && samePlacement(lastPublished, p)) {
91
+ steadyFrames++;
92
+ if (steadyFrames >= STEADY_CLOSE_FRAMES && !pointerHeld) {
93
+ sentinelDeadline = null;
94
+ return;
95
+ }
96
+ } else {
97
+ lastPublished = p;
98
+ publish(p);
99
+ steadyFrames = 0;
100
+ }
101
+ if (!disposed && visible) rafId = requestAnimationFrame(sentinelFrame);
102
+ else sentinelDeadline = null;
103
+ };
104
+ const openSentinel = () => {
105
+ if (!followGeometry || disposed) return;
106
+ steadyFrames = 0;
107
+ if (rafId === null) rafId = requestAnimationFrame(sentinelFrame);
108
+ };
109
+ const closeSentinel = () => {
110
+ if (rafId !== null) {
111
+ cancelAnimationFrame(rafId);
112
+ rafId = null;
113
+ }
114
+ steadyFrames = 0;
115
+ sentinelDeadline = null;
116
+ pointerHeld = false;
117
+ };
118
+ const onScroll = () => {
119
+ if (followGeometry) openSentinel();
120
+ else emit();
121
+ };
122
+ const onPointerDown = (e) => {
123
+ const t = e.target;
124
+ if (t && t.closest && t.closest("[role=\"separator\"]")) {
125
+ pointerHeld = true;
126
+ openSentinel();
127
+ }
128
+ };
129
+ const onPointerUp = () => {
130
+ if (!pointerHeld) return;
131
+ pointerHeld = false;
132
+ openSentinel();
133
+ };
134
+ const startObserving = () => {
135
+ if (observer) return;
136
+ observer = new ResizeObserver(emit);
137
+ observer.observe(target);
138
+ window.addEventListener("resize", emit);
139
+ if (guardDisplayNone && typeof IntersectionObserver !== "undefined") {
140
+ io = new IntersectionObserver(emit);
141
+ io.observe(target);
142
+ }
143
+ if (followScroll) window.addEventListener("scroll", onScroll, {
144
+ capture: true,
145
+ passive: true
146
+ });
147
+ if (followGeometry) {
148
+ window.addEventListener("pointerdown", onPointerDown, { capture: true });
149
+ window.addEventListener("pointerup", onPointerUp, { capture: true });
150
+ }
151
+ };
152
+ const stopObserving = () => {
153
+ if (observer) {
154
+ observer.disconnect();
155
+ observer = null;
156
+ }
157
+ if (io) {
158
+ io.disconnect();
159
+ io = null;
160
+ }
161
+ window.removeEventListener("resize", emit);
162
+ window.removeEventListener("scroll", onScroll, { capture: true });
163
+ window.removeEventListener("pointerdown", onPointerDown, { capture: true });
164
+ window.removeEventListener("pointerup", onPointerUp, { capture: true });
165
+ closeSentinel();
166
+ };
167
+ const apply = () => {
168
+ lastPublished = null;
169
+ if (visible) {
170
+ startObserving();
171
+ lastPublished = computePlacement();
172
+ publish(lastPublished);
173
+ } else {
174
+ stopObserving();
175
+ const hidden = { visible: false };
176
+ lastPublished = hidden;
177
+ publish(hidden);
178
+ }
179
+ };
180
+ apply();
181
+ return {
182
+ update(next) {
183
+ if (disposed) return;
184
+ publish = next.publish;
185
+ visible = next.visible;
186
+ apply();
187
+ },
188
+ dispose() {
189
+ if (disposed) return;
190
+ disposed = true;
191
+ stopObserving();
192
+ },
193
+ pulse(durationMs) {
194
+ if (disposed || !followGeometry) return;
195
+ if (durationMs !== void 0 && durationMs > 0) {
196
+ const next = performance.now() + durationMs;
197
+ sentinelDeadline = sentinelDeadline === null ? next : Math.max(sentinelDeadline, next);
198
+ }
199
+ openSentinel();
200
+ }
201
+ };
202
+ }
203
+ //#endregion
204
+ //#region src/client/layout-client.ts
205
+ /**
206
+ * Renderer half of the A5-2 slot-token handshake (§A5-2.1). Subscribes to main's
207
+ * `slot-grant` pushes FIRST (so a grant replayed synchronously by `subscribe()`
208
+ * cannot be missed), then on each grant anchors the granted DOM slot with this
209
+ * session's hardening opts (followScroll / followGeometry / guardDisplayNone)
210
+ * and threads each measured `Placement` back to main as a `place` carrying that
211
+ * grant's `slotToken`.
212
+ */
213
+ function createDeckLayoutClient(deps) {
214
+ const resolveSlot = deps.resolveSlot ?? ((id) => document.querySelector(id));
215
+ const createAnchor = deps.createAnchor ?? createPlacementAnchor;
216
+ const byViewId = /* @__PURE__ */ new Map();
217
+ let disposed = false;
218
+ const unsub = deps.bridge.onSlotGrant((grant) => {
219
+ if (disposed) return;
220
+ const existing = byViewId.get(grant.viewId);
221
+ if (existing && existing.token === grant.slotToken) return;
222
+ if (existing) try {
223
+ existing.anchor.dispose();
224
+ } finally {
225
+ byViewId.delete(grant.viewId);
226
+ }
227
+ const el = resolveSlot(grant.slotId);
228
+ if (!el) return;
229
+ const anchor = createAnchor(el, {
230
+ visible: true,
231
+ followScroll: true,
232
+ followGeometry: true,
233
+ guardDisplayNone: true,
234
+ publish: (placement) => {
235
+ deps.bridge.sendPlace({
236
+ slotToken: grant.slotToken,
237
+ placement
238
+ });
239
+ }
240
+ });
241
+ byViewId.set(grant.viewId, {
242
+ token: grant.slotToken,
243
+ anchor
244
+ });
245
+ });
246
+ deps.bridge.subscribe();
247
+ return { dispose() {
248
+ if (disposed) return;
249
+ disposed = true;
250
+ unsub();
251
+ for (const { anchor } of byViewId.values()) anchor.dispose();
252
+ byViewId.clear();
253
+ } };
254
+ }
255
+ //#endregion
256
+ //#region src/client/index.ts
257
+ function createDeckClient(options) {
258
+ const globalName = options?.globalName ?? "__electronDeckBridge";
259
+ function getBridge() {
260
+ const bridge = readBridgeFromGlobal(globalName);
261
+ if (bridge === void 0) throw new DeckClientNotReadyError(`Deck framework bridge is missing on window["${globalName}"] — did the host preload call exposeDeckBridge()?`);
262
+ return bridge;
263
+ }
264
+ return {
265
+ async ready() {
266
+ const bridge = getBridge();
267
+ const remoteMajor = parseMajor(bridge.version);
268
+ const localMajor = parseMajor(BRIDGE_PROTOCOL_VERSION);
269
+ if (remoteMajor !== localMajor) throw new DeckClientNotReadyError(`Deck bridge protocol version mismatch: bridge=${String(bridge.version)}, client expected major ${localMajor}.x.x (${BRIDGE_PROTOCOL_VERSION})`);
270
+ },
271
+ async invoke(name, ...args) {
272
+ const response = await getBridge().invoke({
273
+ kind: "host",
274
+ name,
275
+ args
276
+ });
277
+ if (response.ok) return response.result;
278
+ const { remoteName, message, code } = response.error;
279
+ throw new DeckRemoteError(remoteName, message, code);
280
+ },
281
+ on(event, listener) {
282
+ const bridge = getBridge();
283
+ const wrapped = (env) => {
284
+ if (env.name === event.name) listener(env.payload);
285
+ };
286
+ const unsubscribe = bridge.onEvent(wrapped);
287
+ let disposed = false;
288
+ return { dispose: () => {
289
+ if (disposed) return;
290
+ disposed = true;
291
+ unsubscribe();
292
+ } };
293
+ }
294
+ };
295
+ }
296
+ function readBridgeFromGlobal(globalName = DEFAULT_BRIDGE_GLOBAL) {
297
+ const bridge = globalThis[globalName];
298
+ if (bridge === void 0 || bridge === null) return void 0;
299
+ return bridge;
300
+ }
301
+ function parseMajor(version) {
302
+ if (typeof version !== "string") return NaN;
303
+ const head = version.split(".")[0] ?? "";
304
+ const n = Number.parseInt(head, 10);
305
+ return Number.isNaN(n) ? NaN : n;
306
+ }
307
+ //#endregion
308
+ export { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL, DeckClientNotReadyError, DeckRemoteError, createDeckClient, createDeckLayoutClient, readBridgeFromGlobal };
309
+
310
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../view-anchor/src/view-anchor.ts","../../src/client/layout-client.ts","../../src/client/index.ts"],"sourcesContent":["import type {\n Bounds,\n Placement,\n ViewAnchorOptions,\n ViewAnchorHandle,\n} from './types.js'\n\nconst ZERO: Bounds = { x: 0, y: 0, width: 0, height: 0 }\n\n// Round to integers (setBounds rejects fractionals). Width/height are clamped\n// to ≥0 (negative area is meaningless and `0` is the canonical \"hidden\"\n// signal). x/y are NOT clamped: a position is a position — an anchored overlay\n// scrolled past the top/left edge has a legitimately NEGATIVE origin, and\n// flooring it to 0 would pin the native view at the edge instead of letting it\n// track its element off-screen. (Each consumer's IPC schema enforces its own\n// origin policy — the simulator's allows negative; the DevTools placeholder\n// never goes negative, so its NonNegInt schema is unaffected.)\nconst clampRect = (r: {\n x: number\n y: number\n width: number\n height: number\n}): Bounds => ({\n x: Math.round(r.x),\n y: Math.round(r.y),\n width: Math.max(0, Math.round(r.width)),\n height: Math.max(0, Math.round(r.height)),\n})\n\n/**\n * Create an anchor binding ONE native view's bounds to `target`'s geometry.\n *\n * Imperative core — no React, no Electron. Behaviour:\n * - `present === true`: publish `target.getBoundingClientRect()` (x/y rounded,\n * width/height `Math.max(0, Math.round(...))`) immediately, then re-publish\n * SYNCHRONOUSLY on every `ResizeObserver` tick and window `resize`.\n * - `present === false`: publish `{0,0,0,0}` immediately; do not observe.\n * - `update(opts)`: re-apply synchronously.\n * - `dispose()`: stop observing, never publish again.\n *\n * Synchronous, NOT RAF-deferred: the native overlay is a cross-process\n * `WebContentsView` whose `setBounds` already lands ~1 compositor frame behind\n * the renderer's DOM paint (the two processes composite on different frames).\n * Deferring the measure+publish to a RAF stacked a SECOND frame on top — during\n * a height/splitter drag that read as the overlay visibly trailing the region\n * edge (worst when GROWING, where the not-yet-followed edge exposes background).\n * Publishing in the observer tick itself removes that self-inflicted frame and\n * leaves only the unavoidable cross-process frame (masked by matching the\n * placeholder/desk background colour). The anti-flood role the RAF used to play\n * — collapsing a burst of RO+resize ticks in one frame into one publish — is now\n * served by `lastPublished` dedup: a tick whose measured rect is byte-identical\n * to the last published one is dropped, so a continuous drag still emits at most\n * one publish per distinct rect.\n *\n * Teardown safety: there is no queued frame to outrun a state change — every\n * emit reads `disposed`/`present` synchronously, so a tick after\n * `update`/`dispose` can never write a stale rect over the live one.\n */\nexport function createViewAnchor(\n target: HTMLElement,\n opts: ViewAnchorOptions,\n): ViewAnchorHandle {\n let present = opts.present\n let publish = opts.publish\n let observer: ResizeObserver | null = null\n // The last rect handed to `publish`, for dedup-coalescing (see header). Reset\n // to `null` on every `apply()` so a state change (e.g. zoom, which rides in\n // the `publish` closure, not in `Bounds`) always forces one fresh publish\n // even when the geometry is unchanged.\n let lastPublished: Bounds | null = null\n let disposed = false\n\n const measure = (): Bounds => {\n const r = target.getBoundingClientRect()\n return clampRect({ x: r.left, y: r.top, width: r.width, height: r.height })\n }\n\n const sameRect = (a: Bounds, b: Bounds): boolean =>\n a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height\n\n // Measure + publish SYNCHRONOUSLY on the triggering tick — no RAF defer (see\n // header for why). Bail if torn down or detached, and dedup a rect\n // byte-identical to the last published one (collapses a same-frame RO+resize\n // burst, and a steady drag that re-fires the same final rect, into one).\n const emit = (): void => {\n if (disposed || !present) return\n const m = measure()\n if (lastPublished && sameRect(lastPublished, m)) return\n lastPublished = m\n publish(m)\n }\n\n const startObserving = (): void => {\n if (observer) return\n observer = new ResizeObserver(emit)\n observer.observe(target)\n window.addEventListener('resize', emit)\n }\n\n const stopObserving = (): void => {\n if (observer) {\n observer.disconnect()\n observer = null\n }\n window.removeEventListener('resize', emit)\n }\n\n // Apply the current (present, publish) synchronously. Reset `lastPublished`\n // first so the publish below is never dedup-skipped — a state change (zoom,\n // present flip, new publish target) must always re-emit even if the geometry\n // is byte-identical to the previous emit.\n const apply = (): void => {\n lastPublished = null\n if (present) {\n startObserving()\n lastPublished = measure()\n publish(lastPublished)\n } else {\n stopObserving()\n publish(ZERO)\n }\n }\n\n apply()\n\n return {\n update(next: ViewAnchorOptions): void {\n if (disposed) return\n publish = next.publish\n present = next.present\n apply()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n stopObserving()\n },\n }\n}\n\n// ── Explicit Placement API ────────────────────────────────────────────\n//\n// The modern surface that replaces the magic-`{0,0,0,0}` \"hidden\" value.\n// Visibility is an explicit discriminant, NEVER inferred from geometry — a\n// real 0×0-but-on-screen view is `{ visible:true, bounds:{...,width:0} }`\n// and a hidden one is `{ visible:false }` (no `bounds`). See `Placement`.\n\nexport interface PlacementAnchorOptions {\n /**\n * Caller's INTENT: should the native view be on-screen? `true` →\n * publish the measured rect as `{ visible:true, bounds }`; `false` →\n * publish `{ visible:false }`. Crucially, hiddenness comes from this\n * flag, not from a measured zero size — so a legitimately 0-sized but\n * visible target still publishes `visible:true`.\n */\n visible: boolean\n /** Receives each explicit Placement. Owns IPC → host. */\n publish: (placement: Placement) => void\n /**\n * Opt-in geometry detach. When true, a measured zero-area target (no\n * geometry box — display:none / unmounted / unstable first layout) publishes\n * `{ visible:false }` (detach-but-keep) instead of `{ visible:true,\n * bounds:0×0 }`, and an IntersectionObserver is attached so a display:none\n * transition (which ResizeObserver does not report) re-publishes. Default\n * false keeps the legitimate 0×0-visible semantics.\n */\n guardDisplayNone?: boolean\n /**\n * Opt-in capture-phase ancestor-scroll follow (§2.C). When true, the anchor\n * listens for `scroll` on `window` in the CAPTURE phase (scroll events don't\n * bubble, but reach `window` while capturing), so an ancestor scroll\n * container scrolling the target re-measures and re-publishes. With\n * `followGeometry` off, the scroll callback does a single synchronous\n * `emit()`; with it on, the scroll OPENS the RAF sentinel window so the\n * follow tracks every frame of a scroll burst. Default false.\n */\n followScroll?: boolean\n /**\n * Opt-in windowed RAF geometry sentinel (§2.D/§6). Catches ancestor\n * transform / reflow moves that no DOM event reports. The sentinel is\n * NON-resident: it is OPENED on demand (a scroll burst, a `[role=\"separator\"]`\n * splitter pointerdown, or an explicit `pulse()`), polls geometry once per\n * animation frame publishing IN-FRAME, and AUTO-CLOSES once the rect goes\n * steady (a few unchanged frames). While closed it schedules no frame, so the\n * static cost when idle is exactly zero. Default false.\n */\n followGeometry?: boolean\n}\n\nexport interface PlacementAnchorHandle {\n /** Apply new options; re-publishes immediately (mirrors `createViewAnchor`). */\n update(opts: PlacementAnchorOptions): void\n /** Stop observing; never publish again. */\n dispose(): void\n /**\n * Open the RAF sentinel window (animation follow); auto-closes after going\n * steady or after `durationMs`. No-op when `followGeometry` is false.\n */\n pulse(durationMs?: number): void\n}\n\n/**\n * Pure measure: read `target`'s rect and wrap it as an explicit visible\n * Placement. Always `{ visible:true }` — hiddenness is a caller decision\n * (see `createPlacementAnchor`), so this never returns `{ visible:false }`\n * and never infers visibility from a 0 size. A collapsed (0×0) but present\n * element therefore yields `{ visible:true, bounds:{...,width:0,height:0} }`,\n * distinct from any hidden Placement.\n */\nexport function measurePlacement(target: HTMLElement): Placement {\n const r = target.getBoundingClientRect()\n return {\n visible: true,\n bounds: clampRect({ x: r.left, y: r.top, width: r.width, height: r.height }),\n }\n}\n\nconst samePlacement = (a: Placement, b: Placement): boolean => {\n // Discriminant-aware dedup: a visibility flip is always a change, even when\n // the geometry would otherwise look identical.\n if (a.visible !== b.visible) return false\n if (a.visible && b.visible) {\n return (\n a.bounds.x === b.bounds.x &&\n a.bounds.y === b.bounds.y &&\n a.bounds.width === b.bounds.width &&\n a.bounds.height === b.bounds.height\n )\n }\n return true\n}\n\n/**\n * The explicit-Placement mirror of `createViewAnchor`. Same observer/dedup/\n * teardown machinery, but the sink receives a `Placement`:\n * - `visible === true` → publish `measurePlacement(target)` and re-publish\n * SYNCHRONOUSLY on every `ResizeObserver`/`resize` tick.\n * - `visible === false` → publish `{ visible:false }` (NOT a ZERO bounds);\n * do not observe.\n *\n * Dedup carries the discriminant (`samePlacement`), so a visibility flip is\n * never coalesced away.\n *\n * Opt-in `guardDisplayNone` (default false): when on, a measured zero-area\n * target (display:none / unmounted / unstable first layout) publishes\n * `{ visible:false }` instead of `{ visible:true, bounds:0×0 }`, and an\n * IntersectionObserver is attached so a display:none transition (which\n * ResizeObserver does not report) re-publishes.\n */\nexport function createPlacementAnchor(\n target: HTMLElement,\n opts: PlacementAnchorOptions,\n): PlacementAnchorHandle {\n let visible = opts.visible\n let publish = opts.publish\n const guardDisplayNone = opts.guardDisplayNone ?? false\n // Captured at creation; the follow options are never re-set via update().\n const followScroll = opts.followScroll ?? false\n const followGeometry = opts.followGeometry ?? false\n let observer: ResizeObserver | null = null\n let io: IntersectionObserver | null = null\n let lastPublished: Placement | null = null\n let disposed = false\n\n // ── Windowed RAF geometry sentinel state (§2.D/§6) ──────────────────\n // The sentinel is a windowed poll, opened on demand and auto-closing once\n // the geometry goes steady. It publishes IN-FRAME (no nested defer): each\n // frame measures, and either publishes a changed rect synchronously or\n // counts toward the steady-close threshold. While closed `rafId` is null\n // and no frame is scheduled — zero static cost when idle.\n let rafId: number | null = null\n let steadyFrames = 0\n const STEADY_CLOSE_FRAMES = 2\n // True while a [role=\"separator\"] splitter drag is in progress: set on the\n // capture-phase pointerdown that opened the window, cleared on pointerup\n // (§2.D). A held pointer means the drag may still resume after a static\n // pause, so a steady run while held must NOT close the sentinel — it only\n // closes once the pointer is released (松手后 2~3 帧内判静止→关窗). Without\n // this gate a press that pauses a couple of frames before the drag actually\n // moves would close mid-press and drop the entire subsequent drag.\n let pointerHeld = false\n // Absolute time (performance.now()) past which a `pulse(durationMs)` window\n // force-closes even if the geometry is still changing — the upper bound that\n // prevents a perpetually-animating target from keeping the sentinel resident.\n // null = no time bound (scroll/splitter opens rely on steady-close instead).\n let sentinelDeadline: number | null = null\n\n // Measure the target, applying the opt-in first-frame / display:none guard:\n // a zero-area box (no geometry to anchor) becomes a detach instead of a\n // 0×0-visible Placement. Default off → byte-for-byte the plain measure.\n const computePlacement = (): Placement => {\n const p = measurePlacement(target)\n if (\n guardDisplayNone &&\n p.visible &&\n (p.bounds.width === 0 || p.bounds.height === 0)\n ) {\n return { visible: false }\n }\n return p\n }\n\n const emit = (): void => {\n if (disposed || !visible) return\n const p = computePlacement()\n if (lastPublished && samePlacement(lastPublished, p)) return\n lastPublished = p\n publish(p)\n }\n\n // One sentinel frame: measure, publish-in-frame if changed, else count toward\n // the steady-close threshold. A changed frame re-arms; a steady frame re-arms\n // until STEADY_CLOSE_FRAMES consecutive unchanged frames, then closes (no\n // re-arm). Reads `disposed`/`visible` live so a frame outliving teardown is\n // inert.\n const sentinelFrame = (): void => {\n rafId = null\n if (disposed || !visible) {\n sentinelDeadline = null\n return\n }\n // §2.F upper bound: a pulse window past its deadline closes regardless of\n // motion, so a target that changes every frame can't keep the sentinel alive.\n if (sentinelDeadline !== null && performance.now() >= sentinelDeadline) {\n sentinelDeadline = null\n return // duration elapsed → close (no re-arm)\n }\n const p = computePlacement()\n if (lastPublished && samePlacement(lastPublished, p)) {\n steadyFrames++\n // Steady-close only fires once the pointer is RELEASED (§2.D): while a\n // splitter drag is held, a static pause is a hesitation, not the end of\n // the drag, so we keep polling (re-arm below) and let `steadyFrames`\n // accrue — it converges to a close within N frames after pointerup.\n if (steadyFrames >= STEADY_CLOSE_FRAMES && !pointerHeld) {\n sentinelDeadline = null\n return // steady (and released) → close\n }\n } else {\n lastPublished = p\n publish(p) // publish synchronously in THIS frame\n steadyFrames = 0\n }\n // `publish` may have synchronously disposed (or hidden) the anchor; re-read\n // live state so a re-entrant teardown leaves ZERO scheduled frames (B2).\n if (!disposed && visible) {\n rafId = requestAnimationFrame(sentinelFrame) // keep polling\n } else {\n sentinelDeadline = null\n }\n }\n\n const openSentinel = (): void => {\n if (!followGeometry || disposed) return\n steadyFrames = 0\n if (rafId === null) rafId = requestAnimationFrame(sentinelFrame)\n }\n\n const closeSentinel = (): void => {\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n rafId = null\n }\n steadyFrames = 0\n sentinelDeadline = null\n pointerHeld = false\n }\n\n // §2.C — an ancestor scroll moved the target's screen rect. With the sentinel\n // on, open the window so the whole scroll burst is followed frame-by-frame;\n // without it, a single synchronous emit() follows the new rect (A1).\n const onScroll = (): void => {\n if (followGeometry) openSentinel()\n else emit()\n }\n\n // §2.D — a capture-phase pointerdown on a [role=\"separator\"] splitter handle\n // marks the start of a drag that moves the target via ancestor reflow (no RO\n // tick) → open the sentinel.\n const onPointerDown = (e: Event): void => {\n const t = e.target as Element | null\n if (t && t.closest && t.closest('[role=\"separator\"]')) {\n pointerHeld = true\n openSentinel()\n }\n }\n\n // §2.D — pointer released: the drag is over, so a steady run may now close the\n // sentinel. Re-open it (a no-op if already polling) so the steady-close\n // threshold is reached even if the geometry was already static at release.\n const onPointerUp = (): void => {\n if (!pointerHeld) return\n pointerHeld = false\n openSentinel()\n }\n\n const startObserving = (): void => {\n if (observer) return\n observer = new ResizeObserver(emit)\n observer.observe(target)\n window.addEventListener('resize', emit)\n // A display:none transition is invisible to ResizeObserver; an\n // IntersectionObserver re-fires `emit`, which re-measures via\n // `computePlacement` (now-zero box → detach, restored box → visible).\n if (guardDisplayNone && typeof IntersectionObserver !== 'undefined') {\n io = new IntersectionObserver(emit)\n io.observe(target)\n }\n if (followScroll) {\n window.addEventListener('scroll', onScroll, {\n capture: true,\n passive: true,\n })\n }\n if (followGeometry) {\n window.addEventListener('pointerdown', onPointerDown, { capture: true })\n window.addEventListener('pointerup', onPointerUp, { capture: true })\n }\n }\n\n const stopObserving = (): void => {\n if (observer) {\n observer.disconnect()\n observer = null\n }\n if (io) {\n io.disconnect()\n io = null\n }\n window.removeEventListener('resize', emit)\n window.removeEventListener('scroll', onScroll, {\n capture: true,\n } as EventListenerOptions)\n window.removeEventListener('pointerdown', onPointerDown, {\n capture: true,\n } as EventListenerOptions)\n window.removeEventListener('pointerup', onPointerUp, {\n capture: true,\n } as EventListenerOptions)\n closeSentinel()\n }\n\n const apply = (): void => {\n lastPublished = null\n if (visible) {\n startObserving()\n lastPublished = computePlacement()\n publish(lastPublished)\n } else {\n stopObserving()\n const hidden: Placement = { visible: false }\n lastPublished = hidden\n publish(hidden)\n }\n }\n\n apply()\n\n return {\n update(next: PlacementAnchorOptions): void {\n if (disposed) return\n publish = next.publish\n visible = next.visible\n apply()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n stopObserving()\n },\n pulse(durationMs?: number): void {\n // Imperative window open (§2.F): start the animation-follow window. It\n // closes on steady (N=2 unchanged frames) OR, when `durationMs` is given,\n // at that deadline — whichever comes first. The deadline is the upper bound\n // that guarantees a still-animating target cannot keep the sentinel\n // resident; without it, only steady-close applies.\n if (disposed || !followGeometry) return\n if (durationMs !== undefined && durationMs > 0) {\n const next = performance.now() + durationMs\n // Extend (never shorten) an existing window's deadline.\n sentinelDeadline = sentinelDeadline === null ? next : Math.max(sentinelDeadline, next)\n }\n openSentinel()\n },\n }\n}\n","import { createPlacementAnchor } from '@dimina-kit/view-anchor'\nimport type { Placement } from '@dimina-kit/view-anchor'\n\n/** One main→renderer slot grant: a native view (`viewId`) wants to follow the\n * DOM slot `slotId`; `slotToken` is the capability the renderer threads back on\n * every `place` so main can match a placement to the granted slot. */\nexport interface SlotGrant {\n\tviewId: string\n\tslotId: string\n\tslotToken: string\n}\n\n/** The renderer-side transport for the slot-token handshake. `onSlotGrant`\n * registers the grant listener (returns an unsubscribe); `subscribe` asks main\n * to (re)play buffered grants AFTER the listener is attached; `sendPlace`\n * forwards a measured placement carrying its slot's token. */\nexport interface LayoutBridge {\n\tonSlotGrant(cb: (grant: SlotGrant) => void): () => void\n\tsendPlace(msg: { slotToken: string; placement: Placement }): void\n\tsubscribe(): void\n}\n\nexport interface LayoutClientDeps {\n\tbridge: LayoutBridge\n\t/** Resolve a grant's `slotId` to its DOM element. Default:\n\t * `document.querySelector(slotId)`. Returns `null` when the slot is not\n\t * mounted (graceful no-op). */\n\tresolveSlot?(slotId: string): HTMLElement | null\n\t/** Anchor factory. Default: view-anchor's `createPlacementAnchor`. Injected\n\t * in tests to capture `(target, opts)` without real RO/IO/RAF. */\n\tcreateAnchor?: (\n\t\ttarget: HTMLElement,\n\t\topts: {\n\t\t\tvisible: boolean\n\t\t\tpublish: (p: Placement) => void\n\t\t\tfollowScroll?: boolean\n\t\t\tfollowGeometry?: boolean\n\t\t\tguardDisplayNone?: boolean\n\t\t},\n\t) => { dispose(): void }\n}\n\n/**\n * Renderer half of the A5-2 slot-token handshake (§A5-2.1). Subscribes to main's\n * `slot-grant` pushes FIRST (so a grant replayed synchronously by `subscribe()`\n * cannot be missed), then on each grant anchors the granted DOM slot with this\n * session's hardening opts (followScroll / followGeometry / guardDisplayNone)\n * and threads each measured `Placement` back to main as a `place` carrying that\n * grant's `slotToken`.\n */\nexport function createDeckLayoutClient(deps: LayoutClientDeps): {\n\tdispose(): void\n} {\n\tconst resolveSlot =\n\t\tdeps.resolveSlot ??\n\t\t((id: string): HTMLElement | null => document.querySelector(id))\n\tconst createAnchor = deps.createAnchor ?? createPlacementAnchor\n\n // One live anchor per `viewId`. Main intentionally re-delivers a grant on\n // per-wc replay, so we dedup: a same-token replay is a no-op (keep the live\n // anchor); a new token for an existing `viewId` replaces it (dispose old,\n // create new). The map holds only CURRENTLY-LIVE anchors — a replaced anchor\n // is disposed at replacement time and removed, so `dispose()` never\n // double-disposes it.\n const byViewId = new Map<string, { token: string; anchor: { dispose(): void } }>()\n let disposed = false\n\n // Register the grant listener BEFORE requesting replay (handshake §A5-2.1):\n // a grant the main side replays synchronously inside `subscribe()` must find\n // the listener already attached.\n const unsub = deps.bridge.onSlotGrant((grant) => {\n if (disposed) return\n const existing = byViewId.get(grant.viewId)\n // Same viewId+token re-delivered (per-wc replay) → pure no-op: keep the\n // live anchor, do not create a second, do not dispose the first.\n if (existing && existing.token === grant.slotToken) return\n // A new token for this viewId → REVOKE the stale anchor FIRST (codex P4\n // round-3): dispose + drop it BEFORE resolving the new slot, so a stale\n // anchor can never keep publishing a revoked token even when the new slot\n // isn't mounted yet. Deleting before `createAnchor` also means a throw in\n // `createAnchor` can't leave an already-disposed anchor in the map (which a\n // later `dispose()` would double-dispose).\n if (existing) {\n // try/finally (codex P4 round-4): the `delete` MUST run even if the old\n // anchor's dispose throws — otherwise the disposed-but-still-mapped anchor\n // keeps publishing the revoked token AND a later client.dispose() would\n // dispose it a second time.\n try {\n existing.anchor.dispose()\n }\n finally {\n byViewId.delete(grant.viewId)\n }\n }\n const el = resolveSlot(grant.slotId)\n if (!el) return // new slot not mounted → graceful no-op; old anchor already revoked\n const anchor = createAnchor(el, {\n visible: true,\n followScroll: true,\n followGeometry: true,\n guardDisplayNone: true,\n // Capture THIS grant's slotToken in the closure so each anchor publishes\n // to its own token (no cross-talk between slots).\n publish: (placement) => {\n deps.bridge.sendPlace({ slotToken: grant.slotToken, placement })\n },\n })\n byViewId.set(grant.viewId, { token: grant.slotToken, anchor })\n })\n\n // Request replay AFTER the listener is attached.\n deps.bridge.subscribe()\n\n return {\n dispose(): void {\n if (disposed) return\n disposed = true\n unsub()\n for (const { anchor } of byViewId.values()) anchor.dispose()\n byViewId.clear()\n },\n }\n}\n","import {\n\tDeckClientNotReadyError,\n\tDeckRemoteError,\n} from '../errors.js'\nimport {\n\tBRIDGE_PROTOCOL_VERSION,\n\tDEFAULT_BRIDGE_GLOBAL,\n} from '../shared/protocol.js'\nimport type { EventEnvelope, DeckBridge } from '../shared/protocol.js'\nimport type { Disposable, HostEvent, JsonValue } from '../types.js'\n\nexport { DeckClientNotReadyError, DeckRemoteError }\n\nexport { createDeckLayoutClient } from './layout-client.js'\nexport type {\n\tSlotGrant,\n\tLayoutBridge,\n\tLayoutClientDeps,\n} from './layout-client.js'\n\nexport interface CreateDeckClientOptions {\n\t/** 默认 `__electronDeckBridge`;必须与 host preload 对齐 */\n\treadonly globalName?: string\n}\n\n// HS / EV 使用 `any` 而非 `JsonValue[]` 约束:\n// (1) host 侧 HostServiceHandler 同样是 `(...args: any[]) => unknown`,两侧对称;\n// (2) JsonValue 索引签名挡掉常见 host 写法(`(p: { code: string }) => ...`),\n// 强迫 rest-only 签名时 `Parameters<HS[K]>` 推不出真实参数类型;\n// (3) Electron IPC 实际走 structured clone(非 JSON),原约束的\"前提\"本就错;\n// (4) 真要 runtime 校验,在 transport 边界用 `runtime.ipc.handle({ validator })`\n// 与 protocol envelope(`InvokeRequest.args: readonly JsonValue[]`)已经够。\nexport interface DeckClient<\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tHS extends Record<keyof HS, (...args: any[]) => unknown>,\n\tEV extends readonly HostEvent<JsonValue>[],\n> {\n\tready(): Promise<void>\n\tinvoke<K extends keyof HS & string>(\n\t\tname: K,\n\t\t...args: Parameters<HS[K]>\n\t): Promise<Awaited<ReturnType<HS[K]>>>\n\ton<E extends EV[number]>(\n\t\tevent: E,\n\t\tlistener: (payload: E extends HostEvent<infer P> ? P : never) => void,\n\t): Disposable\n}\n\nexport function createDeckClient<\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tHS extends Record<keyof HS, (...args: any[]) => unknown>,\n\tEV extends readonly HostEvent<JsonValue>[],\n>(options?: CreateDeckClientOptions): DeckClient<HS, EV> {\n\tconst globalName = options?.globalName ?? DEFAULT_BRIDGE_GLOBAL\n\n\tfunction getBridge(): DeckBridge {\n\t\tconst bridge = readBridgeFromGlobal(globalName)\n\t\tif (bridge === undefined) {\n\t\t\tthrow new DeckClientNotReadyError(\n\t\t\t\t`Deck framework bridge is missing on window[\"${globalName}\"] — did the host preload call exposeDeckBridge()?`,\n\t\t\t)\n\t\t}\n\t\treturn bridge\n\t}\n\n\treturn {\n\t\tasync ready(): Promise<void> {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst remoteMajor = parseMajor(bridge.version)\n\t\t\tconst localMajor = parseMajor(BRIDGE_PROTOCOL_VERSION)\n\t\t\tif (remoteMajor !== localMajor) {\n\t\t\t\tthrow new DeckClientNotReadyError(\n\t\t\t\t\t`Deck bridge protocol version mismatch: bridge=${String(bridge.version)}, client expected major ${localMajor}.x.x (${BRIDGE_PROTOCOL_VERSION})`,\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\tasync invoke<K extends keyof HS & string>(\n\t\t\tname: K,\n\t\t\t...args: Parameters<HS[K]>\n\t\t): Promise<Awaited<ReturnType<HS[K]>>> {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst response = await bridge.invoke({\n\t\t\t\tkind: 'host',\n\t\t\t\tname,\n\t\t\t\targs: args as readonly JsonValue[],\n\t\t\t})\n\t\t\tif (response.ok) {\n\t\t\t\treturn response.result as Awaited<ReturnType<HS[K]>>\n\t\t\t}\n\t\t\tconst { remoteName, message, code } = response.error\n\t\t\tthrow new DeckRemoteError(remoteName, message, code)\n\t\t},\n\n\t\ton<E extends EV[number]>(\n\t\t\tevent: E,\n\t\t\tlistener: (payload: E extends HostEvent<infer P> ? P : never) => void,\n\t\t): Disposable {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst wrapped = (env: EventEnvelope): void => {\n\t\t\t\tif (env.name === event.name) {\n\t\t\t\t\tlistener(env.payload as E extends HostEvent<infer P> ? P : never)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst unsubscribe = bridge.onEvent(wrapped)\n\t\t\tlet disposed = false\n\t\t\treturn {\n\t\t\t\tdispose: () => {\n\t\t\t\t\tif (disposed) return\n\t\t\t\t\tdisposed = true\n\t\t\t\t\tunsubscribe()\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n}\n\nexport function readBridgeFromGlobal(\n\tglobalName: string = DEFAULT_BRIDGE_GLOBAL,\n): DeckBridge | undefined {\n\tconst g = globalThis as unknown as Record<string, unknown>\n\tconst bridge = g[globalName]\n\tif (bridge === undefined || bridge === null) return undefined\n\treturn bridge as DeckBridge\n}\n\nfunction parseMajor(version: unknown): number {\n\tif (typeof version !== 'string') return Number.NaN\n\tconst head = version.split('.')[0] ?? ''\n\tconst n = Number.parseInt(head, 10)\n\treturn Number.isNaN(n) ? Number.NaN : n\n}\n\nexport { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL }\n"],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { Placement } from '@dimina-kit/view-anchor';
2
+ /** One main→renderer slot grant: a native view (`viewId`) wants to follow the
3
+ * DOM slot `slotId`; `slotToken` is the capability the renderer threads back on
4
+ * every `place` so main can match a placement to the granted slot. */
5
+ export interface SlotGrant {
6
+ viewId: string;
7
+ slotId: string;
8
+ slotToken: string;
9
+ }
10
+ /** The renderer-side transport for the slot-token handshake. `onSlotGrant`
11
+ * registers the grant listener (returns an unsubscribe); `subscribe` asks main
12
+ * to (re)play buffered grants AFTER the listener is attached; `sendPlace`
13
+ * forwards a measured placement carrying its slot's token. */
14
+ export interface LayoutBridge {
15
+ onSlotGrant(cb: (grant: SlotGrant) => void): () => void;
16
+ sendPlace(msg: {
17
+ slotToken: string;
18
+ placement: Placement;
19
+ }): void;
20
+ subscribe(): void;
21
+ }
22
+ export interface LayoutClientDeps {
23
+ bridge: LayoutBridge;
24
+ /** Resolve a grant's `slotId` to its DOM element. Default:
25
+ * `document.querySelector(slotId)`. Returns `null` when the slot is not
26
+ * mounted (graceful no-op). */
27
+ resolveSlot?(slotId: string): HTMLElement | null;
28
+ /** Anchor factory. Default: view-anchor's `createPlacementAnchor`. Injected
29
+ * in tests to capture `(target, opts)` without real RO/IO/RAF. */
30
+ createAnchor?: (target: HTMLElement, opts: {
31
+ visible: boolean;
32
+ publish: (p: Placement) => void;
33
+ followScroll?: boolean;
34
+ followGeometry?: boolean;
35
+ guardDisplayNone?: boolean;
36
+ }) => {
37
+ dispose(): void;
38
+ };
39
+ }
40
+ /**
41
+ * Renderer half of the A5-2 slot-token handshake (§A5-2.1). Subscribes to main's
42
+ * `slot-grant` pushes FIRST (so a grant replayed synchronously by `subscribe()`
43
+ * cannot be missed), then on each grant anchors the granted DOM slot with this
44
+ * session's hardening opts (followScroll / followGeometry / guardDisplayNone)
45
+ * and threads each measured `Placement` back to main as a `place` carrying that
46
+ * grant's `slotToken`.
47
+ */
48
+ export declare function createDeckLayoutClient(deps: LayoutClientDeps): {
49
+ dispose(): void;
50
+ };
51
+ //# sourceMappingURL=layout-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-client.d.ts","sourceRoot":"","sources":["../../src/client/layout-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD;;uEAEuE;AACvE,MAAM,WAAW,SAAS;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CACjB;AAED;;;+DAG+D;AAC/D,MAAM,WAAW,YAAY;IAC5B,WAAW,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IACvD,SAAS,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI,CAAA;IACjE,SAAS,IAAI,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,YAAY,CAAA;IACpB;;oCAEgC;IAChC,WAAW,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAA;IAChD;uEACmE;IACnE,YAAY,CAAC,EAAE,CACd,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE;QACL,OAAO,EAAE,OAAO,CAAA;QAChB,OAAO,EAAE,CAAC,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;QAC/B,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAC1B,KACG;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,CAAA;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,gBAAgB,GAAG;IAC/D,OAAO,IAAI,IAAI,CAAA;CACf,CAsEA"}