@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/client/index.d.ts +20 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +310 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/layout-client.d.ts +51 -0
- package/dist/client/layout-client.d.ts.map +1 -0
- package/dist/electron-deck-DfFKeFEC.js +2295 -0
- package/dist/electron-deck-DfFKeFEC.js.map +1 -0
- package/dist/electron-deck.d.ts +57 -0
- package/dist/electron-deck.d.ts.map +1 -0
- package/dist/errors-NE2ig7L5.js +53 -0
- package/dist/errors-NE2ig7L5.js.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/events.d.ts +14 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/host/capability.d.ts +50 -0
- package/dist/host/capability.d.ts.map +1 -0
- package/dist/host/control-bus.d.ts +92 -0
- package/dist/host/control-bus.d.ts.map +1 -0
- package/dist/host/index.d.ts +42 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/internal/deck-app.d.ts +415 -0
- package/dist/internal/deck-app.d.ts.map +1 -0
- package/dist/internal/electron-types.d.ts +131 -0
- package/dist/internal/electron-types.d.ts.map +1 -0
- package/dist/internal/event-bus.d.ts +29 -0
- package/dist/internal/event-bus.d.ts.map +1 -0
- package/dist/internal/ipc-registry-memory.d.ts +25 -0
- package/dist/internal/ipc-registry-memory.d.ts.map +1 -0
- package/dist/internal/lifecycle-manager.d.ts +28 -0
- package/dist/internal/lifecycle-manager.d.ts.map +1 -0
- package/dist/internal/resource-registry.d.ts +9 -0
- package/dist/internal/resource-registry.d.ts.map +1 -0
- package/dist/internal/trust-set.d.ts +53 -0
- package/dist/internal/trust-set.d.ts.map +1 -0
- package/dist/internal/wire-transport.d.ts +159 -0
- package/dist/internal/wire-transport.d.ts.map +1 -0
- package/dist/main/compositor.d.ts +123 -0
- package/dist/main/compositor.d.ts.map +1 -0
- package/dist/main/connection.d.ts +37 -0
- package/dist/main/connection.d.ts.map +1 -0
- package/dist/main/debug-tap.d.ts +55 -0
- package/dist/main/debug-tap.d.ts.map +1 -0
- package/dist/main/disposable.d.ts +18 -0
- package/dist/main/disposable.d.ts.map +1 -0
- package/dist/main/index.d.ts +16 -0
- package/dist/main/index.d.ts.map +1 -0
- package/dist/main/index.js +152 -0
- package/dist/main/index.js.map +1 -0
- package/dist/main/logger.d.ts +15 -0
- package/dist/main/logger.d.ts.map +1 -0
- package/dist/main/scope.d.ts +52 -0
- package/dist/main/scope.d.ts.map +1 -0
- package/dist/main/view-handle.d.ts +131 -0
- package/dist/main/view-handle.d.ts.map +1 -0
- package/dist/preload/index.cjs +105 -0
- package/dist/preload/index.cjs.map +1 -0
- package/dist/preload/index.d.ts +43 -0
- package/dist/preload/index.d.ts.map +1 -0
- package/dist/preload/index.js +80 -0
- package/dist/preload/index.js.map +1 -0
- package/dist/protocol-BJqo-vP2.js +23 -0
- package/dist/protocol-BJqo-vP2.js.map +1 -0
- package/dist/shared/protocol.d.ts +76 -0
- package/dist/shared/protocol.d.ts.map +1 -0
- package/dist/types.d.ts +504 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/view-handle-CR-yWNfr.js +701 -0
- package/dist/view-handle-CR-yWNfr.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
//#region src/main/disposable.ts
|
|
2
|
+
function toDisposable(fn) {
|
|
3
|
+
let done = false;
|
|
4
|
+
return { dispose() {
|
|
5
|
+
if (done) return;
|
|
6
|
+
done = true;
|
|
7
|
+
return fn();
|
|
8
|
+
} };
|
|
9
|
+
}
|
|
10
|
+
var DisposableRegistry = class {
|
|
11
|
+
entries = [];
|
|
12
|
+
_disposed = false;
|
|
13
|
+
/**
|
|
14
|
+
* Number of live entries. A wrapper's `dispose` splices its entry out and
|
|
15
|
+
* `disposeAll` clears the array, so `entries.length` is the live count.
|
|
16
|
+
*/
|
|
17
|
+
get size() {
|
|
18
|
+
return this.entries.length;
|
|
19
|
+
}
|
|
20
|
+
add(d) {
|
|
21
|
+
if (this._disposed) throw new Error("cannot add to disposed registry");
|
|
22
|
+
const fn = typeof d === "function" ? d : () => d.dispose();
|
|
23
|
+
const entry = {
|
|
24
|
+
fn,
|
|
25
|
+
released: false
|
|
26
|
+
};
|
|
27
|
+
this.entries.push(entry);
|
|
28
|
+
return { dispose: () => {
|
|
29
|
+
if (entry.released) return;
|
|
30
|
+
entry.released = true;
|
|
31
|
+
const i = this.entries.indexOf(entry);
|
|
32
|
+
if (i >= 0) this.entries.splice(i, 1);
|
|
33
|
+
return fn();
|
|
34
|
+
} };
|
|
35
|
+
}
|
|
36
|
+
async disposeAll() {
|
|
37
|
+
if (this._disposed) return;
|
|
38
|
+
this._disposed = true;
|
|
39
|
+
const items = this.entries.slice().reverse();
|
|
40
|
+
this.entries = [];
|
|
41
|
+
const errors = [];
|
|
42
|
+
for (const entry of items) {
|
|
43
|
+
if (entry.released) continue;
|
|
44
|
+
entry.released = true;
|
|
45
|
+
try {
|
|
46
|
+
await entry.fn();
|
|
47
|
+
} catch (e) {
|
|
48
|
+
errors.push(e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (errors.length > 0) throw new AggregateError(errors, "DisposableRegistry encountered errors during disposeAll");
|
|
52
|
+
}
|
|
53
|
+
dispose() {
|
|
54
|
+
return this.disposeAll();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/main/logger.ts
|
|
59
|
+
var LEVEL_PRIORITY = {
|
|
60
|
+
debug: 0,
|
|
61
|
+
info: 1,
|
|
62
|
+
warn: 2,
|
|
63
|
+
error: 3
|
|
64
|
+
};
|
|
65
|
+
var LEVEL_PREFIX = {
|
|
66
|
+
debug: "[DEBUG]",
|
|
67
|
+
info: "[INFO]",
|
|
68
|
+
warn: "[WARN]",
|
|
69
|
+
error: "[ERROR]"
|
|
70
|
+
};
|
|
71
|
+
var currentLevel = "debug";
|
|
72
|
+
function shouldLog(level) {
|
|
73
|
+
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel];
|
|
74
|
+
}
|
|
75
|
+
function formatMessage(level, tag, message) {
|
|
76
|
+
return `${(/* @__PURE__ */ new Date()).toISOString()} ${LEVEL_PREFIX[level]} [${tag}] ${message}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a tagged logger for a specific module.
|
|
80
|
+
* All output goes to console with structured prefix.
|
|
81
|
+
*/
|
|
82
|
+
function createLogger(tag) {
|
|
83
|
+
return {
|
|
84
|
+
debug(message, ...args) {
|
|
85
|
+
if (shouldLog("debug")) console.debug(formatMessage("debug", tag, message), ...args);
|
|
86
|
+
},
|
|
87
|
+
info(message, ...args) {
|
|
88
|
+
if (shouldLog("info")) console.info(formatMessage("info", tag, message), ...args);
|
|
89
|
+
},
|
|
90
|
+
warn(message, ...args) {
|
|
91
|
+
if (shouldLog("warn")) console.warn(formatMessage("warn", tag, message), ...args);
|
|
92
|
+
},
|
|
93
|
+
error(message, ...args) {
|
|
94
|
+
if (shouldLog("error")) console.error(formatMessage("error", tag, message), ...args);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function setLogLevel(level) {
|
|
99
|
+
currentLevel = level;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/main/scope.ts
|
|
103
|
+
/**
|
|
104
|
+
* `Scope` — engine-agnostic nested-lifetime primitive, generalizing the
|
|
105
|
+
* Connection/Disposable semantics in this package (foundation.md §4).
|
|
106
|
+
*
|
|
107
|
+
* A `Scope` owns a single "lifetime segment". Resources bound via `own()` and
|
|
108
|
+
* sub-scopes created via `child()` are released together — both on `reset()`
|
|
109
|
+
* (soft reuse: dispose the segment, open a fresh one, stay alive) and `close()`
|
|
110
|
+
* (terminal: dispose everything, die). Teardown is LIFO and, crucially,
|
|
111
|
+
* `reset()`/`close()` are COMPLETION FENCES: their Promise resolves (and the
|
|
112
|
+
* `'reset'`/`'closed'` listeners fire) only AFTER the underlying async
|
|
113
|
+
* disposeAll has fully finished — unlike `connection.ts`, which fires the
|
|
114
|
+
* teardown and forgets (`void ...disposeAll()`).
|
|
115
|
+
*
|
|
116
|
+
* Cross-layer LIFO: a scope's teardown disposes its child sub-scopes (deepest
|
|
117
|
+
* first, recursively) BEFORE its own directly-owned resources, so a grandchild
|
|
118
|
+
* tears down before a child, which tears down before the root.
|
|
119
|
+
*/
|
|
120
|
+
var log = createLogger("scope");
|
|
121
|
+
function asInternal(s) {
|
|
122
|
+
return s;
|
|
123
|
+
}
|
|
124
|
+
/** True if `maybeAncestor` is `node` or an ancestor of it (walking parents). */
|
|
125
|
+
function isSelfOrAncestor(maybeAncestor, node) {
|
|
126
|
+
let cur = node;
|
|
127
|
+
while (cur) {
|
|
128
|
+
if (cur === maybeAncestor) return true;
|
|
129
|
+
cur = cur.__parent().parent;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
var NOOP_DISPOSABLE = { dispose() {} };
|
|
134
|
+
function toDispose(d) {
|
|
135
|
+
return typeof d === "function" ? d : () => d.dispose();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Dispose a resource handed to a dead scope's `own()` — immediately, exactly
|
|
139
|
+
* once, never delegating to a disposed segment (that would throw). Both sync
|
|
140
|
+
* throws and async rejections are caught/logged so a late teardown can never
|
|
141
|
+
* escape as an unhandledRejection in the main process.
|
|
142
|
+
*/
|
|
143
|
+
function disposeLate(d) {
|
|
144
|
+
try {
|
|
145
|
+
const r = toDispose(d)();
|
|
146
|
+
if (r && typeof r.then === "function") r.catch((e) => log.error("late own() async disposer rejected", e));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
log.error("late own() resource disposer threw", e);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function newSegment() {
|
|
152
|
+
return {
|
|
153
|
+
children: [],
|
|
154
|
+
resources: new DisposableRegistry()
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function disposeSegment(segment) {
|
|
158
|
+
const children = segment.children.slice().reverse();
|
|
159
|
+
segment.children = [];
|
|
160
|
+
for (const child of children) try {
|
|
161
|
+
await child.close();
|
|
162
|
+
} catch (e) {
|
|
163
|
+
log.error("child scope close threw during segment teardown", e);
|
|
164
|
+
}
|
|
165
|
+
await segment.resources.disposeAll();
|
|
166
|
+
}
|
|
167
|
+
function createScope() {
|
|
168
|
+
let segment = newSegment();
|
|
169
|
+
let alive = true;
|
|
170
|
+
let inFlight = null;
|
|
171
|
+
let inFlightKind = null;
|
|
172
|
+
const resetListeners = /* @__PURE__ */ new Set();
|
|
173
|
+
const closedListeners = /* @__PURE__ */ new Set();
|
|
174
|
+
let parent = null;
|
|
175
|
+
let owningSegment = null;
|
|
176
|
+
const childRemovers = /* @__PURE__ */ new Map();
|
|
177
|
+
function emit(ev) {
|
|
178
|
+
const set = ev === "reset" ? resetListeners : closedListeners;
|
|
179
|
+
for (const cb of [...set]) try {
|
|
180
|
+
cb();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
log.error(`listener for "${ev}" threw`, e);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function runReset() {
|
|
186
|
+
const old = segment;
|
|
187
|
+
segment = newSegment();
|
|
188
|
+
inFlightKind = "reset";
|
|
189
|
+
const p = (async () => {
|
|
190
|
+
try {
|
|
191
|
+
await disposeSegment(old);
|
|
192
|
+
} finally {
|
|
193
|
+
if (inFlightKind === "reset") {
|
|
194
|
+
inFlight = null;
|
|
195
|
+
inFlightKind = null;
|
|
196
|
+
emit("reset");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
inFlight = p;
|
|
201
|
+
return p;
|
|
202
|
+
}
|
|
203
|
+
function runClose() {
|
|
204
|
+
alive = false;
|
|
205
|
+
const old = segment;
|
|
206
|
+
segment = newSegment();
|
|
207
|
+
inFlightKind = "close";
|
|
208
|
+
const p = (async () => {
|
|
209
|
+
try {
|
|
210
|
+
await disposeSegment(old);
|
|
211
|
+
} finally {
|
|
212
|
+
inFlight = null;
|
|
213
|
+
inFlightKind = null;
|
|
214
|
+
emit("closed");
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
inFlight = p;
|
|
218
|
+
return p;
|
|
219
|
+
}
|
|
220
|
+
function attachChild(child, seg) {
|
|
221
|
+
seg.children.push(child);
|
|
222
|
+
const remover = child.on("closed", () => {
|
|
223
|
+
const i = seg.children.indexOf(child);
|
|
224
|
+
if (i >= 0) seg.children.splice(i, 1);
|
|
225
|
+
childRemovers.delete(child);
|
|
226
|
+
});
|
|
227
|
+
childRemovers.set(child, remover);
|
|
228
|
+
child.__setParent(scope, seg);
|
|
229
|
+
}
|
|
230
|
+
function detachChild(child, seg) {
|
|
231
|
+
const i = seg.children.indexOf(child);
|
|
232
|
+
if (i >= 0) seg.children.splice(i, 1);
|
|
233
|
+
const remover = childRemovers.get(child);
|
|
234
|
+
if (remover) {
|
|
235
|
+
remover.dispose();
|
|
236
|
+
childRemovers.delete(child);
|
|
237
|
+
}
|
|
238
|
+
child.__setParent(null, null);
|
|
239
|
+
}
|
|
240
|
+
const scope = {
|
|
241
|
+
get alive() {
|
|
242
|
+
return alive;
|
|
243
|
+
},
|
|
244
|
+
__currentSegment() {
|
|
245
|
+
return segment;
|
|
246
|
+
},
|
|
247
|
+
__inFlight() {
|
|
248
|
+
return inFlight;
|
|
249
|
+
},
|
|
250
|
+
__parent() {
|
|
251
|
+
return {
|
|
252
|
+
parent,
|
|
253
|
+
owningSegment
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
__attachChild(child, seg) {
|
|
257
|
+
attachChild(child, seg);
|
|
258
|
+
},
|
|
259
|
+
__detachChild(child, seg) {
|
|
260
|
+
detachChild(child, seg);
|
|
261
|
+
},
|
|
262
|
+
__setParent(p, seg) {
|
|
263
|
+
parent = p;
|
|
264
|
+
owningSegment = seg;
|
|
265
|
+
},
|
|
266
|
+
own(d) {
|
|
267
|
+
if (!alive) {
|
|
268
|
+
disposeLate(d);
|
|
269
|
+
return NOOP_DISPOSABLE;
|
|
270
|
+
}
|
|
271
|
+
return segment.resources.add(d);
|
|
272
|
+
},
|
|
273
|
+
child() {
|
|
274
|
+
if (!alive) {
|
|
275
|
+
const dead = createScope();
|
|
276
|
+
dead.close();
|
|
277
|
+
return dead;
|
|
278
|
+
}
|
|
279
|
+
const sub = asInternal(createScope());
|
|
280
|
+
attachChild(sub, segment);
|
|
281
|
+
return sub;
|
|
282
|
+
},
|
|
283
|
+
reset() {
|
|
284
|
+
if (!alive) return inFlight ?? Promise.resolve();
|
|
285
|
+
if (inFlight) return inFlight;
|
|
286
|
+
return runReset();
|
|
287
|
+
},
|
|
288
|
+
close() {
|
|
289
|
+
if (!alive) return inFlight ?? Promise.resolve();
|
|
290
|
+
if (inFlight && inFlightKind === "close") return inFlight;
|
|
291
|
+
if (inFlight && inFlightKind === "reset") {
|
|
292
|
+
alive = false;
|
|
293
|
+
const afterReset = inFlight;
|
|
294
|
+
inFlightKind = "close";
|
|
295
|
+
const upgraded = (async () => {
|
|
296
|
+
await afterReset.catch(() => {});
|
|
297
|
+
const leftover = segment;
|
|
298
|
+
segment = newSegment();
|
|
299
|
+
try {
|
|
300
|
+
await disposeSegment(leftover);
|
|
301
|
+
} finally {
|
|
302
|
+
inFlight = null;
|
|
303
|
+
inFlightKind = null;
|
|
304
|
+
emit("closed");
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
inFlight = upgraded;
|
|
308
|
+
return upgraded;
|
|
309
|
+
}
|
|
310
|
+
return runClose();
|
|
311
|
+
},
|
|
312
|
+
on(ev, cb) {
|
|
313
|
+
const set = ev === "reset" ? resetListeners : closedListeners;
|
|
314
|
+
set.add(cb);
|
|
315
|
+
let removed = false;
|
|
316
|
+
return { dispose() {
|
|
317
|
+
if (removed) return;
|
|
318
|
+
removed = true;
|
|
319
|
+
set.delete(cb);
|
|
320
|
+
} };
|
|
321
|
+
},
|
|
322
|
+
async adopt(childPublic, newParentPublic) {
|
|
323
|
+
const child = asInternal(childPublic);
|
|
324
|
+
const newParent = asInternal(newParentPublic);
|
|
325
|
+
const self = scope;
|
|
326
|
+
while (true) {
|
|
327
|
+
const f = self.__inFlight() ?? newParent.__inFlight();
|
|
328
|
+
if (!f) break;
|
|
329
|
+
await f.catch(() => {});
|
|
330
|
+
}
|
|
331
|
+
if (!self.alive) throw new Error("adopt: donor scope is not alive");
|
|
332
|
+
if (!newParent.alive) throw new Error("adopt: newParent scope is not alive");
|
|
333
|
+
const fromSegment = self.__currentSegment();
|
|
334
|
+
const toSegment = newParent.__currentSegment();
|
|
335
|
+
if (child.__parent().parent !== self || fromSegment.children.indexOf(child) < 0) throw new Error("adopt: child is not a direct child of this scope current segment");
|
|
336
|
+
if (isSelfOrAncestor(child, newParent)) throw new Error("adopt: would create a cycle (newParent is child or a descendant of it)");
|
|
337
|
+
self.__detachChild(child, fromSegment);
|
|
338
|
+
newParent.__attachChild(child, toSegment);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
return scope;
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/main/compositor.ts
|
|
345
|
+
/**
|
|
346
|
+
* Typed failure thrown by {@link Compositor.commit}. `kind` distinguishes a
|
|
347
|
+
* preflight refusal (`host-destroyed`, native byte-for-byte pre-commit,
|
|
348
|
+
* `applied:false`) from a mid-apply native throw (`apply-failed`,
|
|
349
|
+
* `applied:'partial'`); for `apply-failed`, `recovered` reports whether the
|
|
350
|
+
* best-effort rollback restored the pre-apply snapshot order (`false` ⇒ native
|
|
351
|
+
* is untrusted and the host must be treated as dead).
|
|
352
|
+
*/
|
|
353
|
+
var CommitError = class extends Error {
|
|
354
|
+
kind;
|
|
355
|
+
applied;
|
|
356
|
+
recovered;
|
|
357
|
+
constructor(args) {
|
|
358
|
+
super(args.message ?? `commit failed: ${args.kind}`);
|
|
359
|
+
this.name = "CommitError";
|
|
360
|
+
this.kind = args.kind;
|
|
361
|
+
this.applied = args.applied;
|
|
362
|
+
if (args.recovered !== void 0) this.recovered = args.recovered;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
var DEFAULT_ZONE = 0;
|
|
366
|
+
/**
|
|
367
|
+
* Smallest representable gap between fractional keys before we renumber. Once a
|
|
368
|
+
* midpoint would round to (or below) this distance from a neighbor, the zone is
|
|
369
|
+
* rebalanced rather than risk key collision / loss of ordering.
|
|
370
|
+
*/
|
|
371
|
+
var MIN_KEY_GAP = 1e-9;
|
|
372
|
+
function createCompositor(host) {
|
|
373
|
+
const views = /* @__PURE__ */ new Map();
|
|
374
|
+
let mountSeqCounter = 0;
|
|
375
|
+
/** Views in a zone, sorted by the in-zone order `(orderKey, viewId)`. */
|
|
376
|
+
function zoneSorted(zone) {
|
|
377
|
+
const out = [];
|
|
378
|
+
for (const v of views.values()) if (v.zone === zone) out.push(v);
|
|
379
|
+
out.sort(compareInZone);
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
function compareInZone(a, b) {
|
|
383
|
+
if (a.orderKey !== b.orderKey) return a.orderKey - b.orderKey;
|
|
384
|
+
return a.ref.id < b.ref.id ? -1 : a.ref.id > b.ref.id ? 1 : 0;
|
|
385
|
+
}
|
|
386
|
+
/** Full target order: zones ascending (low renders first/bottom), then
|
|
387
|
+
* in-zone order. */
|
|
388
|
+
function targetOrder() {
|
|
389
|
+
const all = [...views.values()];
|
|
390
|
+
all.sort((a, b) => {
|
|
391
|
+
if (a.zone !== b.zone) return a.zone - b.zone;
|
|
392
|
+
return compareInZone(a, b);
|
|
393
|
+
});
|
|
394
|
+
return all;
|
|
395
|
+
}
|
|
396
|
+
/** Next order key that lands a view at the TOP (end) of its zone. */
|
|
397
|
+
function topKey(zone) {
|
|
398
|
+
const z = zoneSorted(zone);
|
|
399
|
+
const last = z[z.length - 1];
|
|
400
|
+
return last === void 0 ? 0 : last.orderKey + 1;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Renumber a zone to evenly-spaced integer keys, preserving the current
|
|
404
|
+
* relative order exactly. Invisible: the visible order is unchanged, so a
|
|
405
|
+
* commit after a rebalance produces no extra host churn.
|
|
406
|
+
*/
|
|
407
|
+
function rebalanceZone(zone) {
|
|
408
|
+
zoneSorted(zone).forEach((v, i) => {
|
|
409
|
+
v.orderKey = i;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Compute the fractional key that places a view immediately before `anchor`
|
|
414
|
+
* within its zone (the midpoint between `anchor` and its predecessor), or at
|
|
415
|
+
* the zone top when `anchor` is null. If the midpoint would exhaust precision,
|
|
416
|
+
* rebalance the zone and recompute on the fresh integer keys.
|
|
417
|
+
*/
|
|
418
|
+
function keyBefore(zone, anchor, movingId) {
|
|
419
|
+
if (anchor === null) return topKey(zone);
|
|
420
|
+
const midpointBefore = (anchorId) => {
|
|
421
|
+
const z = zoneSorted(zone).filter((v) => v.ref.id !== movingId);
|
|
422
|
+
const idx = z.findIndex((v) => v.ref.id === anchorId);
|
|
423
|
+
const at = z[idx];
|
|
424
|
+
if (idx < 0 || at === void 0) return null;
|
|
425
|
+
const hiKey = at.orderKey;
|
|
426
|
+
const prev = z[idx - 1];
|
|
427
|
+
const loKey = prev !== void 0 ? prev.orderKey : hiKey - 1;
|
|
428
|
+
const mid = (loKey + hiKey) / 2;
|
|
429
|
+
if (mid <= loKey + MIN_KEY_GAP || mid >= hiKey - MIN_KEY_GAP) return null;
|
|
430
|
+
return mid;
|
|
431
|
+
};
|
|
432
|
+
const first = midpointBefore(anchor.ref.id);
|
|
433
|
+
if (first === null && zoneSorted(zone).some((v) => v.ref.id === anchor.ref.id)) {
|
|
434
|
+
rebalanceZone(zone);
|
|
435
|
+
const retry = midpointBefore(anchor.ref.id);
|
|
436
|
+
if (retry !== null) return retry;
|
|
437
|
+
}
|
|
438
|
+
return first ?? topKey(zone);
|
|
439
|
+
}
|
|
440
|
+
const compositor = {
|
|
441
|
+
mount(view, opts) {
|
|
442
|
+
const zone = opts?.zone ?? DEFAULT_ZONE;
|
|
443
|
+
if (views.get(view.id)) return;
|
|
444
|
+
views.set(view.id, {
|
|
445
|
+
ref: view,
|
|
446
|
+
zone,
|
|
447
|
+
orderKey: topKey(zone),
|
|
448
|
+
mountSeq: ++mountSeqCounter
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
unmount(viewId) {
|
|
452
|
+
views.delete(viewId);
|
|
453
|
+
},
|
|
454
|
+
reorder(viewId, opts) {
|
|
455
|
+
const mv = views.get(viewId);
|
|
456
|
+
if (!mv) throw new Error(`reorder: view "${viewId}" is not mounted`);
|
|
457
|
+
const before = opts.before;
|
|
458
|
+
const targetZone = opts.zone ?? mv.zone;
|
|
459
|
+
if (before === void 0 || before === null) {
|
|
460
|
+
mv.zone = targetZone;
|
|
461
|
+
mv.orderKey = topKey(targetZone);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const anchor = views.get(before);
|
|
465
|
+
if (!anchor) throw new Error(`reorder: before anchor "${before}" is not mounted`);
|
|
466
|
+
if (opts.zone !== void 0 && anchor.zone !== opts.zone) throw new Error(`reorder: before anchor "${before}" is in zone ${anchor.zone}, which conflicts with the requested zone ${opts.zone}`);
|
|
467
|
+
mv.zone = anchor.zone;
|
|
468
|
+
mv.orderKey = keyBefore(anchor.zone, anchor, viewId);
|
|
469
|
+
},
|
|
470
|
+
commit() {
|
|
471
|
+
const target = targetOrder();
|
|
472
|
+
const targetIds = target.map((v) => v.ref.id);
|
|
473
|
+
const current = host.children().map((v) => v.id);
|
|
474
|
+
const targetSet = new Set(targetIds);
|
|
475
|
+
const currentSet = new Set(current);
|
|
476
|
+
const removals = [];
|
|
477
|
+
const additions = [];
|
|
478
|
+
const shared = targetIds.filter((id) => currentSet.has(id));
|
|
479
|
+
const currentIndexOf = /* @__PURE__ */ new Map();
|
|
480
|
+
current.forEach((id, i) => currentIndexOf.set(id, i));
|
|
481
|
+
const keepIds = /* @__PURE__ */ new Set();
|
|
482
|
+
let prevPos = -1;
|
|
483
|
+
for (const id of shared) {
|
|
484
|
+
const pos = currentIndexOf.get(id);
|
|
485
|
+
if (pos > prevPos) {
|
|
486
|
+
keepIds.add(id);
|
|
487
|
+
prevPos = pos;
|
|
488
|
+
} else break;
|
|
489
|
+
}
|
|
490
|
+
for (const v of host.children()) if (!targetSet.has(v.id)) removals.push(v);
|
|
491
|
+
target.forEach((v, i) => {
|
|
492
|
+
const isNew = !currentSet.has(v.ref.id);
|
|
493
|
+
const mustMove = currentSet.has(v.ref.id) && !keepIds.has(v.ref.id);
|
|
494
|
+
if (isNew || mustMove) additions.push({
|
|
495
|
+
ref: v.ref,
|
|
496
|
+
atIndex: i
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
if (removals.length === 0 && additions.length === 0) return;
|
|
500
|
+
if (host.isDestroyed) {
|
|
501
|
+
if (additions.length === 0) return;
|
|
502
|
+
throw new CommitError({
|
|
503
|
+
kind: "host-destroyed",
|
|
504
|
+
applied: false,
|
|
505
|
+
message: "commit: contentView is destroyed"
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const snapshot = host.children().slice();
|
|
509
|
+
additions.sort((a, b) => a.atIndex - b.atIndex);
|
|
510
|
+
try {
|
|
511
|
+
for (const r of removals) host.removeChildView(r);
|
|
512
|
+
for (const a of additions) host.addChildView(a.ref);
|
|
513
|
+
} catch (applyErr) {
|
|
514
|
+
let recovered;
|
|
515
|
+
try {
|
|
516
|
+
for (const v of host.children().slice()) host.removeChildView(v);
|
|
517
|
+
for (const v of snapshot) host.addChildView(v);
|
|
518
|
+
recovered = true;
|
|
519
|
+
} catch {
|
|
520
|
+
recovered = false;
|
|
521
|
+
}
|
|
522
|
+
throw new CommitError({
|
|
523
|
+
kind: "apply-failed",
|
|
524
|
+
applied: "partial",
|
|
525
|
+
recovered,
|
|
526
|
+
message: `commit apply failed: ${applyErr?.message ?? applyErr}`
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
detachAll() {
|
|
531
|
+
for (const id of [...views.keys()]) views.delete(id);
|
|
532
|
+
compositor.commit();
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
return compositor;
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/main/view-handle.ts
|
|
539
|
+
function createViewHandle(deps) {
|
|
540
|
+
const { nativeView } = deps;
|
|
541
|
+
const ref = nativeView.ref;
|
|
542
|
+
let current = null;
|
|
543
|
+
let currentZone;
|
|
544
|
+
let viewScope = null;
|
|
545
|
+
let owningWindowScope = null;
|
|
546
|
+
let active = false;
|
|
547
|
+
let visibleBounds = null;
|
|
548
|
+
let migrating = false;
|
|
549
|
+
let lockChain = Promise.resolve();
|
|
550
|
+
function withLock(fn) {
|
|
551
|
+
const run = lockChain.then(fn, fn);
|
|
552
|
+
lockChain = run.then(() => {}, () => {});
|
|
553
|
+
return run;
|
|
554
|
+
}
|
|
555
|
+
function ensureMounted() {
|
|
556
|
+
if (!current) return;
|
|
557
|
+
current.compositor.mount(ref, { zone: currentZone });
|
|
558
|
+
current.compositor.commit();
|
|
559
|
+
}
|
|
560
|
+
async function doDispose() {
|
|
561
|
+
if (!viewScope) return;
|
|
562
|
+
await viewScope.close();
|
|
563
|
+
}
|
|
564
|
+
async function doMove(dest, opts) {
|
|
565
|
+
if (!active || !current || !viewScope) throw new Error("moveTo: the view is not currently placed (disposed or never placed)");
|
|
566
|
+
const src = current;
|
|
567
|
+
const srcZone = currentZone;
|
|
568
|
+
const destZone = opts.zone;
|
|
569
|
+
const vs = viewScope;
|
|
570
|
+
src.compositor.unmount(ref.id);
|
|
571
|
+
try {
|
|
572
|
+
src.compositor.commit();
|
|
573
|
+
} catch (e) {
|
|
574
|
+
src.compositor.mount(ref, { zone: srcZone });
|
|
575
|
+
try {
|
|
576
|
+
src.compositor.commit();
|
|
577
|
+
} catch {
|
|
578
|
+
await doDispose();
|
|
579
|
+
throw e;
|
|
580
|
+
}
|
|
581
|
+
throw e;
|
|
582
|
+
}
|
|
583
|
+
dest.compositor.mount(ref, { zone: destZone });
|
|
584
|
+
try {
|
|
585
|
+
dest.compositor.commit();
|
|
586
|
+
} catch (destErr) {
|
|
587
|
+
dest.compositor.unmount(ref.id);
|
|
588
|
+
src.compositor.mount(ref, { zone: srcZone });
|
|
589
|
+
try {
|
|
590
|
+
src.compositor.commit();
|
|
591
|
+
} catch {
|
|
592
|
+
await doDispose();
|
|
593
|
+
throw destErr;
|
|
594
|
+
}
|
|
595
|
+
throw destErr;
|
|
596
|
+
}
|
|
597
|
+
current = {
|
|
598
|
+
compositor: dest.compositor,
|
|
599
|
+
windowScope: dest.windowScope
|
|
600
|
+
};
|
|
601
|
+
currentZone = destZone;
|
|
602
|
+
if (opts.rehome) {
|
|
603
|
+
const donor = owningWindowScope ?? src.windowScope;
|
|
604
|
+
try {
|
|
605
|
+
await donor.adopt(vs, dest.windowScope);
|
|
606
|
+
} catch (adoptErr) {
|
|
607
|
+
dest.compositor.unmount(ref.id);
|
|
608
|
+
try {
|
|
609
|
+
dest.compositor.commit();
|
|
610
|
+
} catch {}
|
|
611
|
+
src.compositor.mount(ref, { zone: srcZone });
|
|
612
|
+
current = src;
|
|
613
|
+
currentZone = srcZone;
|
|
614
|
+
try {
|
|
615
|
+
src.compositor.commit();
|
|
616
|
+
} catch {
|
|
617
|
+
await doDispose();
|
|
618
|
+
throw adoptErr;
|
|
619
|
+
}
|
|
620
|
+
throw adoptErr;
|
|
621
|
+
}
|
|
622
|
+
owningWindowScope = dest.windowScope;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const handle = {
|
|
626
|
+
placeIn(target, opts) {
|
|
627
|
+
if (viewScope) throw new Error("ViewHandle.placeIn: view already placed — use moveTo() to migrate");
|
|
628
|
+
current = {
|
|
629
|
+
compositor: target.compositor,
|
|
630
|
+
windowScope: target.windowScope
|
|
631
|
+
};
|
|
632
|
+
currentZone = opts.zone;
|
|
633
|
+
ensureMounted();
|
|
634
|
+
active = true;
|
|
635
|
+
const vs = target.windowScope.child();
|
|
636
|
+
viewScope = vs;
|
|
637
|
+
owningWindowScope = target.windowScope;
|
|
638
|
+
vs.own(() => {
|
|
639
|
+
if (current) {
|
|
640
|
+
current.compositor.unmount(ref.id);
|
|
641
|
+
current.compositor.commit();
|
|
642
|
+
}
|
|
643
|
+
deps.nativeView.destroy?.();
|
|
644
|
+
});
|
|
645
|
+
vs.own(() => {
|
|
646
|
+
active = false;
|
|
647
|
+
});
|
|
648
|
+
vs.own(() => {
|
|
649
|
+
deps.onDispose?.();
|
|
650
|
+
});
|
|
651
|
+
return handle;
|
|
652
|
+
},
|
|
653
|
+
applyPlacement(p) {
|
|
654
|
+
if (!active || !current) return;
|
|
655
|
+
if (migrating) return;
|
|
656
|
+
if (p.visible) {
|
|
657
|
+
ensureMounted();
|
|
658
|
+
nativeView.setBounds(p.bounds);
|
|
659
|
+
visibleBounds = {
|
|
660
|
+
x: p.bounds.x,
|
|
661
|
+
y: p.bounds.y,
|
|
662
|
+
width: p.bounds.width,
|
|
663
|
+
height: p.bounds.height
|
|
664
|
+
};
|
|
665
|
+
} else {
|
|
666
|
+
current.compositor.unmount(ref.id);
|
|
667
|
+
current.compositor.commit();
|
|
668
|
+
visibleBounds = null;
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
moveTo(dest, opts) {
|
|
672
|
+
return withLock(async () => {
|
|
673
|
+
migrating = true;
|
|
674
|
+
try {
|
|
675
|
+
return await doMove(dest, opts);
|
|
676
|
+
} finally {
|
|
677
|
+
migrating = false;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
},
|
|
681
|
+
dispose() {
|
|
682
|
+
return withLock(() => doDispose());
|
|
683
|
+
},
|
|
684
|
+
get webContents() {
|
|
685
|
+
return nativeView.webContents;
|
|
686
|
+
},
|
|
687
|
+
bounds() {
|
|
688
|
+
if (!active || !visibleBounds) return null;
|
|
689
|
+
return { ...visibleBounds };
|
|
690
|
+
},
|
|
691
|
+
capturePage() {
|
|
692
|
+
if (!nativeView.capturePage) return Promise.reject(/* @__PURE__ */ new Error("ViewHandle.capturePage: native view has no capturePage"));
|
|
693
|
+
return nativeView.capturePage();
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
return handle;
|
|
697
|
+
}
|
|
698
|
+
//#endregion
|
|
699
|
+
export { createLogger as a, toDisposable as c, createScope as i, CommitError as n, setLogLevel as o, createCompositor as r, DisposableRegistry as s, createViewHandle as t };
|
|
700
|
+
|
|
701
|
+
//# sourceMappingURL=view-handle-CR-yWNfr.js.map
|