@dimina-kit/devtools 0.4.0-dev.20260728110120 → 0.4.0-dev.20260731100034
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/main/api.d.ts +1 -0
- package/dist/main/app/app.d.ts +3 -0
- package/dist/main/app/app.js +15 -0
- package/dist/main/app/bootstrap.d.ts +8 -5
- package/dist/main/app/bootstrap.js +15 -15
- package/dist/main/index.bundle.js +2359 -5132
- package/dist/main/ipc/app-lifecycle.d.ts +1 -31
- package/dist/main/ipc/app-lifecycle.js +1 -53
- package/dist/main/ipc/bridge-router.d.ts +19 -152
- package/dist/main/ipc/bridge-router.js +28 -2186
- package/dist/main/ipc/page-scroll.d.ts +1 -13
- package/dist/main/ipc/page-scroll.js +1 -16
- package/dist/main/ipc/session-listener-bag.d.ts +1 -40
- package/dist/main/ipc/session-listener-bag.js +1 -39
- package/dist/main/services/console-forward/index.d.ts +1 -60
- package/dist/main/services/console-forward/index.js +1 -225
- package/dist/main/services/console-forward/internal-log.d.ts +1 -10
- package/dist/main/services/console-forward/internal-log.js +1 -22
- package/dist/main/services/diagnostics/index.d.ts +1 -58
- package/dist/main/services/diagnostics/index.js +1 -60
- package/dist/main/services/dimina-resource-server.d.ts +1 -6
- package/dist/main/services/dimina-resource-server.js +1 -98
- package/dist/main/services/network-forward/index.d.ts +15 -3
- package/dist/main/services/network-forward/index.js +30 -4
- package/dist/main/services/network-forward/websocket.d.ts +36 -0
- package/dist/main/services/network-forward/websocket.js +120 -0
- package/dist/main/services/service-host-pool/pool.d.ts +1 -196
- package/dist/main/services/service-host-pool/pool.js +1 -363
- package/dist/main/services/simulator/custom-apis.d.ts +1 -42
- package/dist/main/services/simulator/custom-apis.js +1 -46
- package/dist/main/services/simulator/referer.d.ts +1 -9
- package/dist/main/services/simulator/referer.js +1 -19
- package/dist/main/services/simulator/ui-extensions.d.ts +13 -0
- package/dist/main/services/simulator/ui-extensions.js +166 -0
- package/dist/main/services/simulator-storage/service-storage-ops.d.ts +1 -39
- package/dist/main/services/simulator-storage/service-storage-ops.js +1 -66
- package/dist/main/services/simulator-temp-files/disk.d.ts +1 -54
- package/dist/main/services/simulator-temp-files/disk.js +1 -237
- package/dist/main/services/simulator-temp-files/fs-channels.d.ts +1 -68
- package/dist/main/services/simulator-temp-files/fs-channels.js +1 -107
- package/dist/main/services/simulator-temp-files/index.d.ts +1 -32
- package/dist/main/services/simulator-temp-files/index.js +1 -181
- package/dist/main/services/simulator-temp-files/request-handler.d.ts +1 -29
- package/dist/main/services/simulator-temp-files/request-handler.js +1 -177
- package/dist/main/services/simulator-temp-files/resolver.d.ts +1 -21
- package/dist/main/services/simulator-temp-files/resolver.js +1 -21
- package/dist/main/services/simulator-temp-files/store.d.ts +1 -13
- package/dist/main/services/simulator-temp-files/store.js +1 -23
- package/dist/main/services/views/miniapp-partition.d.ts +1 -89
- package/dist/main/services/views/miniapp-partition.js +1 -158
- package/dist/main/services/views/native-simulator-view.js +3 -0
- package/dist/main/services/views/simulator-session-policy.d.ts +1 -27
- package/dist/main/services/views/simulator-session-policy.js +1 -98
- package/dist/main/services/views/view-manager.d.ts +5 -0
- package/dist/main/services/workbench-context.d.ts +10 -1
- package/dist/main/services/workbench-context.js +20 -0
- package/dist/main/utils/ipc-registry.d.ts +21 -3
- package/dist/main/utils/ipc-registry.js +26 -23
- package/dist/main/utils/paths.d.ts +14 -0
- package/dist/main/utils/paths.js +14 -0
- package/dist/main/windows/service-host-window/create.d.ts +1 -92
- package/dist/main/windows/service-host-window/create.js +1 -154
- package/dist/native-host/render/render.js +51 -52
- package/dist/native-host/service/service.js +2 -2
- package/dist/preload/index.d.ts +2 -0
- package/dist/preload/index.js +1 -0
- package/dist/preload/runtime/clipboard.d.ts +6 -0
- package/dist/preload/runtime/clipboard.js +12 -0
- package/dist/preload/runtime/native-host.js +4 -10
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs +46 -126
- package/dist/preload/windows/main.cjs.map +3 -3
- package/dist/preload/windows/simulator.cjs +83 -56
- package/dist/preload/windows/simulator.cjs.map +4 -4
- package/dist/preload/windows/simulator.js +83 -56
- package/dist/renderer/assets/{index-BJlFbXHb.js → index-BBg3YI5b.js} +2 -2
- package/dist/renderer/entries/main/index.html +1 -1
- package/dist/shared/bridge-channels.d.ts +1 -419
- package/dist/shared/bridge-channels.js +1 -89
- package/dist/shared/dmb-resource-url.d.ts +2 -0
- package/dist/shared/dmb-resource-url.js +2 -0
- package/dist/shared/ipc-channels.d.ts +2 -32
- package/dist/shared/ipc-channels.js +2 -5
- package/dist/shared/request-core.d.ts +1 -83
- package/dist/shared/request-core.js +1 -167
- package/dist/shared/simulator-ui.d.ts +61 -0
- package/dist/shared/simulator-ui.js +12 -0
- package/dist/shared/types.d.ts +7 -0
- package/dist/simulator/assets/device-shell-B03OuytR.js +2 -0
- package/dist/simulator/assets/device-shell-DbC5ccs8.css +1 -0
- package/dist/simulator/assets/{jsx-runtime-BDTY6fEq.js → jsx-runtime-hBv-rHqQ.js} +2 -2
- package/dist/simulator/assets/{simulator-CRyLSz1N.js → simulator-DpdXUf--.js} +5 -5
- package/dist/simulator/simulator.html +2 -2
- package/dist/simulator/temp-files.d.ts +29 -0
- package/dist/simulator/temp-files.js +79 -0
- package/dist/simulator/types.d.ts +43 -0
- package/dist/simulator/types.js +2 -0
- package/package.json +10 -5
- package/dist/shared/simulator-api-metadata.d.ts +0 -26
- package/dist/shared/simulator-api-metadata.js +0 -59
- package/dist/shared/vpath.d.ts +0 -42
- package/dist/shared/vpath.js +0 -105
- package/dist/simulator/assets/device-shell-DOy53Y0s.css +0 -1
- package/dist/simulator/assets/device-shell-Sbkjw8Nb.js +0 -2
|
@@ -1,364 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/** Blank placeholder URL loaded while warming / after reset. */
|
|
3
|
-
const BLANK_URL = 'about:blank';
|
|
4
|
-
/** Hard ceiling on pool size regardless of requested size (see prewarm-webview.md). */
|
|
5
|
-
const HARD_MAX_POOL_SIZE = 4;
|
|
6
|
-
/** Storage buckets cleared on every release (see prewarm-webview.md's reset checklist). */
|
|
7
|
-
const RESET_STORAGES = [
|
|
8
|
-
'cookies',
|
|
9
|
-
'localstorage',
|
|
10
|
-
'indexdb',
|
|
11
|
-
'serviceworkers',
|
|
12
|
-
'cachestorage',
|
|
13
|
-
];
|
|
14
|
-
export class ServiceHostPool {
|
|
15
|
-
entries = new Map();
|
|
16
|
-
currentSpec = null;
|
|
17
|
-
targetSize = 0;
|
|
18
|
-
maxPoolSize = 3;
|
|
19
|
-
nextId = 0;
|
|
20
|
-
disposed = false;
|
|
21
|
-
/**
|
|
22
|
-
* Warm `defaultPoolSize` entries with `defaultSpec`. Normally resolves once the
|
|
23
|
-
* pool has reached `defaultPoolSize` ready entries — even if a warming window
|
|
24
|
-
* crashes mid-init and is refilled. NOTE: `warmUpToTarget` caps its refill loop
|
|
25
|
-
* with a bounded guard, so under SUSTAINED warm crashes `init` resolves
|
|
26
|
-
* best-effort (possibly under-filled) rather than spinning forever — treat
|
|
27
|
-
* `getStats().ready` as advisory after init, not a hard invariant.
|
|
28
|
-
*/
|
|
29
|
-
async init(opts) {
|
|
30
|
-
if (this.disposed)
|
|
31
|
-
return;
|
|
32
|
-
this.currentSpec = opts.defaultSpec;
|
|
33
|
-
this.maxPoolSize = Math.min(opts.maxPoolSize ?? 3, HARD_MAX_POOL_SIZE);
|
|
34
|
-
this.targetSize = Math.min(Math.max(opts.defaultPoolSize, 0), this.maxPoolSize);
|
|
35
|
-
await this.warmUpToTarget();
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Hand a ready window matching `spec` to the caller. If none is ready, fall
|
|
39
|
-
* back to synchronously constructing a fresh window — never blocking on warm
|
|
40
|
-
* (see prewarm-webview.md's invariants section). Fallback windows carry `entryId === null` and
|
|
41
|
-
* are destroyed (not pooled) on release.
|
|
42
|
-
*
|
|
43
|
-
* A spec change (different `preloadPath`) tears down all *pooled* entries and
|
|
44
|
-
* re-targets the pool to the new spec; in-use entries are left to their owners
|
|
45
|
-
* and disposed when returned (see `release`).
|
|
46
|
-
*/
|
|
47
|
-
async acquire(spec) {
|
|
48
|
-
if (this.currentSpec && this.currentSpec.preloadPath !== spec.preloadPath) {
|
|
49
|
-
this.onSpecChange(spec);
|
|
50
|
-
}
|
|
51
|
-
else if (!this.currentSpec) {
|
|
52
|
-
this.currentSpec = spec;
|
|
53
|
-
}
|
|
54
|
-
for (const entry of this.entries.values()) {
|
|
55
|
-
if (entry.state === 'ready' && this.matches(entry.spec, spec)) {
|
|
56
|
-
// A ready entry whose window died (closed/crashed) while idle must never
|
|
57
|
-
// be handed out — navigating a destroyed WebContents silently no-ops the
|
|
58
|
-
// spawn. Drop it (and refill) and keep scanning.
|
|
59
|
-
if (entry.win.isDestroyed()) {
|
|
60
|
-
this.reclaim(entry, { destroyIfPooled: false, refill: false });
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
entry.state = 'in-use';
|
|
64
|
-
return { win: entry.win, entryId: entry.id };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Pool miss → fallback create. Not pooled; entryId null.
|
|
68
|
-
const win = this.createWindow(spec);
|
|
69
|
-
// Kick the load but do not await — acquire must not block.
|
|
70
|
-
void this.loadBlank(win);
|
|
71
|
-
return { win, entryId: null };
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Return a window. Fallback windows (`entryId === null`) and entries whose
|
|
75
|
-
* spec no longer matches the current pool spec are torn down. Otherwise the
|
|
76
|
-
* window is reset (navigated to blank, then storage cleared) and re-pooled as
|
|
77
|
-
* `ready`, unless that would exceed `maxPoolSize`.
|
|
78
|
-
*/
|
|
79
|
-
async release(entryId, win) {
|
|
80
|
-
if (entryId === null) {
|
|
81
|
-
this.destroyWindow(win);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const entry = this.entries.get(entryId);
|
|
85
|
-
if (!entry) {
|
|
86
|
-
this.destroyWindow(win);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (this.disposed || !this.currentSpec || !this.matches(entry.spec, this.currentSpec)) {
|
|
90
|
-
this.disposeEntry(entry);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
entry.state = 'resetting';
|
|
94
|
-
await this.reset(entry);
|
|
95
|
-
// The entry may have been disposed/crashed/spec-changed/destroyed underneath
|
|
96
|
-
// the reset await; only re-pool if it is still the live, resetting entry with
|
|
97
|
-
// a live window. A destroyed window must never be re-marked `ready` (a later
|
|
98
|
-
// acquire would hand it out and the spawn would silently never boot).
|
|
99
|
-
if (this.disposed
|
|
100
|
-
|| this.entries.get(entryId) !== entry
|
|
101
|
-
|| entry.state !== 'resetting'
|
|
102
|
-
|| entry.win.isDestroyed()) {
|
|
103
|
-
this.destroyWindow(entry.win);
|
|
104
|
-
this.entries.delete(entryId);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
// Honor the hard cap: if re-pooling would exceed maxPoolSize, drop it.
|
|
108
|
-
const otherPooled = this.warmCount() - (this.isWarm(entry) ? 1 : 0);
|
|
109
|
-
if (otherPooled >= this.maxPoolSize) {
|
|
110
|
-
this.disposeEntry(entry);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
entry.state = 'ready';
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* The owner of an acquired window reports it died externally (graceful close /
|
|
117
|
-
* crash) — reclaim its in-use slot so it isn't leaked in `entries` forever
|
|
118
|
-
* (bridge-router calls this on the `serviceAlreadyClosed` teardown path, where
|
|
119
|
-
* `release` is intentionally skipped). No-op for an unknown id; idempotent;
|
|
120
|
-
* never touches the (already-gone) window.
|
|
121
|
-
*/
|
|
122
|
-
releaseDestroyed(entryId) {
|
|
123
|
-
if (entryId === null)
|
|
124
|
-
return;
|
|
125
|
-
const entry = this.entries.get(entryId);
|
|
126
|
-
if (!entry)
|
|
127
|
-
return;
|
|
128
|
-
this.reclaim(entry, { destroyIfPooled: false, refill: false });
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Re-target the pool. `target` is clamped to `[0, maxPoolSize]`. Pooled
|
|
132
|
-
* entries beyond the target are disposed OLDEST-FIRST (see prewarm-webview.md); in-use
|
|
133
|
-
* entries are untouched. Does not warm new entries up (use `init` / refill).
|
|
134
|
-
*/
|
|
135
|
-
resize(target) {
|
|
136
|
-
this.targetSize = Math.min(Math.max(target, 0), this.maxPoolSize);
|
|
137
|
-
// Map iteration is insertion order, so `pooled` is oldest-first. Dispose the
|
|
138
|
-
// oldest prefix and keep the `targetSize` most-recently-created entries.
|
|
139
|
-
const pooled = [...this.entries.values()].filter((e) => this.isWarm(e));
|
|
140
|
-
const surplus = pooled.length - this.targetSize;
|
|
141
|
-
if (surplus <= 0)
|
|
142
|
-
return;
|
|
143
|
-
for (const entry of pooled.slice(0, surplus)) {
|
|
144
|
-
this.disposeEntry(entry);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/** Occupancy snapshot. */
|
|
148
|
-
getStats() {
|
|
149
|
-
let ready = 0;
|
|
150
|
-
let inUse = 0;
|
|
151
|
-
let warming = 0;
|
|
152
|
-
let resetting = 0;
|
|
153
|
-
for (const entry of this.entries.values()) {
|
|
154
|
-
switch (entry.state) {
|
|
155
|
-
case 'ready':
|
|
156
|
-
ready++;
|
|
157
|
-
break;
|
|
158
|
-
case 'in-use':
|
|
159
|
-
inUse++;
|
|
160
|
-
break;
|
|
161
|
-
case 'warming':
|
|
162
|
-
warming++;
|
|
163
|
-
break;
|
|
164
|
-
case 'resetting':
|
|
165
|
-
resetting++;
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return { total: this.entries.size, ready, inUse, warming, resetting, spec: this.currentSpec };
|
|
170
|
-
}
|
|
171
|
-
/** Tear down every entry and stop accepting work. */
|
|
172
|
-
async dispose() {
|
|
173
|
-
this.disposed = true;
|
|
174
|
-
this.targetSize = 0;
|
|
175
|
-
for (const entry of [...this.entries.values()]) {
|
|
176
|
-
this.disposeEntry(entry);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
// ── internals ──────────────────────────────────────────────────────────
|
|
180
|
-
matches(a, b) {
|
|
181
|
-
return a.preloadPath === b.preloadPath;
|
|
182
|
-
}
|
|
183
|
-
/** ready + warming + resetting entries are "pooled" (owned by the pool). */
|
|
184
|
-
isWarm(entry) {
|
|
185
|
-
return entry.state === 'ready' || entry.state === 'warming' || entry.state === 'resetting';
|
|
186
|
-
}
|
|
187
|
-
warmCount() {
|
|
188
|
-
let n = 0;
|
|
189
|
-
for (const entry of this.entries.values()) {
|
|
190
|
-
if (this.isWarm(entry))
|
|
191
|
-
n++;
|
|
192
|
-
}
|
|
193
|
-
return n;
|
|
194
|
-
}
|
|
195
|
-
readyCount() {
|
|
196
|
-
let n = 0;
|
|
197
|
-
for (const entry of this.entries.values()) {
|
|
198
|
-
if (entry.state === 'ready')
|
|
199
|
-
n++;
|
|
200
|
-
}
|
|
201
|
-
return n;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Drive the pool to `targetSize` ready entries and resolve only then. Loops so
|
|
205
|
-
* a crash that drops a warming entry mid-init (which refills asynchronously
|
|
206
|
-
* via `onGone`) is awaited too, instead of resolving on a stale snapshot. A
|
|
207
|
-
* generous iteration guard prevents an unbounded spin if windows keep dying.
|
|
208
|
-
*/
|
|
209
|
-
async warmUpToTarget() {
|
|
210
|
-
if (!this.currentSpec)
|
|
211
|
-
return;
|
|
212
|
-
let guard = 0;
|
|
213
|
-
const maxIterations = this.targetSize * 8 + 8;
|
|
214
|
-
while (!this.disposed && this.readyCount() < this.targetSize && guard++ < maxIterations) {
|
|
215
|
-
while (this.warmCount() < this.targetSize) {
|
|
216
|
-
void this.warmOne(this.currentSpec);
|
|
217
|
-
}
|
|
218
|
-
const warming = [...this.entries.values()]
|
|
219
|
-
.filter((e) => e.state === 'warming')
|
|
220
|
-
.map((e) => e.ready);
|
|
221
|
-
if (warming.length === 0)
|
|
222
|
-
break;
|
|
223
|
-
await Promise.all(warming);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
createWindow(spec) {
|
|
227
|
-
return new BrowserWindow({
|
|
228
|
-
show: false,
|
|
229
|
-
width: spec.size?.width,
|
|
230
|
-
height: spec.size?.height,
|
|
231
|
-
webPreferences: {
|
|
232
|
-
preload: spec.preloadPath,
|
|
233
|
-
partition: spec.partition,
|
|
234
|
-
// Mirror createServiceHostWindow's process model (create.ts:24-30): the
|
|
235
|
-
// service-host preload does `require('electron')` and writes globals
|
|
236
|
-
// onto the page realm, so it requires nodeIntegration:false,
|
|
237
|
-
// contextIsolation:false, sandbox:false. Default to that contract; let a
|
|
238
|
-
// caller override via the spec for a different runtime.
|
|
239
|
-
nodeIntegration: spec.nodeIntegration ?? false,
|
|
240
|
-
contextIsolation: spec.contextIsolation ?? false,
|
|
241
|
-
sandbox: spec.sandbox ?? false,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
loadBlank(win) {
|
|
246
|
-
// `loadURL` resolves on did-finish-load in Electron; treat any failure as a
|
|
247
|
-
// settled load so a single bad warm never wedges the pool. The synchronous
|
|
248
|
-
// call ITSELF throws on an already-destroyed webContents (it isn't a
|
|
249
|
-
// rejection) — swallow that too, so reset/release of a window that died
|
|
250
|
-
// mid-flight resolves instead of rejecting (the "never wedges" contract).
|
|
251
|
-
try {
|
|
252
|
-
return Promise.resolve(win.webContents.loadURL(BLANK_URL)).then(() => undefined, () => undefined);
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
return Promise.resolve();
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
/** Construct + load a fresh pooled entry, registering crash recovery. */
|
|
259
|
-
warmOne(spec) {
|
|
260
|
-
const win = this.createWindow(spec);
|
|
261
|
-
const id = `pool-${++this.nextId}`;
|
|
262
|
-
const entry = { id, spec, state: 'warming', win, ready: Promise.resolve() };
|
|
263
|
-
this.entries.set(id, entry);
|
|
264
|
-
win.webContents.on('render-process-gone', () => this.onGone(entry));
|
|
265
|
-
// A window can also die by graceful close (app quit / manual close), which
|
|
266
|
-
// never fires render-process-gone. Observe it too so a pooled entry isn't
|
|
267
|
-
// leaked. reclaim is a no-op if WE disposed it (disposeEntry deletes the
|
|
268
|
-
// entry before its own destroy fires 'closed').
|
|
269
|
-
win.once('closed', () => this.reclaim(entry, { destroyIfPooled: false, refill: false }));
|
|
270
|
-
entry.ready = this.loadBlank(win).then(() => {
|
|
271
|
-
if (entry.state === 'warming')
|
|
272
|
-
entry.state = 'ready';
|
|
273
|
-
});
|
|
274
|
-
return entry.ready;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Reset a released window back to a clean, blank state. Order matters
|
|
278
|
-
* (see prewarm-webview.md's state-machine and reset-ordering sections): navigate to blank FIRST so the old document
|
|
279
|
-
* stops running and in-flight requests are aborted, THEN clear storage — so a
|
|
280
|
-
* still-live old page cannot re-populate storage after the clear.
|
|
281
|
-
*/
|
|
282
|
-
async reset(entry) {
|
|
283
|
-
await this.loadBlank(entry.win);
|
|
284
|
-
// Consumers sharing a session opt out of the storage clear (see
|
|
285
|
-
// ServiceHostSpec.clearStorageOnReset): clearing a shared partition would wipe
|
|
286
|
-
// other live projects. Navigation-to-blank above still resets the JS realm.
|
|
287
|
-
if (entry.spec.clearStorageOnReset === false)
|
|
288
|
-
return;
|
|
289
|
-
// The window may have been destroyed during the load await; touching a dead
|
|
290
|
-
// webContents.session throws SYNCHRONOUSLY, which would reject release().
|
|
291
|
-
if (entry.win.isDestroyed())
|
|
292
|
-
return;
|
|
293
|
-
try {
|
|
294
|
-
const ses = entry.win.webContents.session;
|
|
295
|
-
await ses.clearStorageData({ storages: [...RESET_STORAGES] });
|
|
296
|
-
await ses.clearCache();
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
// Window/session torn down mid-reset — release() re-checks isDestroyed and
|
|
300
|
-
// disposes the entry, so swallowing here keeps release from rejecting.
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/** A spec change tears down pooled entries and re-targets the pool. */
|
|
304
|
-
onSpecChange(spec) {
|
|
305
|
-
for (const entry of [...this.entries.values()]) {
|
|
306
|
-
if (this.isWarm(entry))
|
|
307
|
-
this.disposeEntry(entry);
|
|
308
|
-
}
|
|
309
|
-
this.currentSpec = spec;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Reclaim an entry whose window died externally (render crash, graceful close,
|
|
313
|
-
* or an owner-reported destruction). A POOLED death frees a spare → refill
|
|
314
|
-
* toward target; an IN-USE death is the caller's window → drop the bookkeeping
|
|
315
|
-
* WITHOUT destroying it from under them, and do not refill (the spare count is
|
|
316
|
-
* unaffected). `destroyIfPooled` destroys the husk for a pooled render crash
|
|
317
|
-
* (the BrowserWindow object can outlive a dead render process); skip it for a
|
|
318
|
-
* `'closed'`/owner-reported window that is already gone. No-op if the entry was
|
|
319
|
-
* already disposed by us (disposeEntry deletes before its own destroy fires
|
|
320
|
-
* `'closed'`, so this finds nothing).
|
|
321
|
-
*/
|
|
322
|
-
reclaim(entry, opts) {
|
|
323
|
-
if (!this.entries.has(entry.id))
|
|
324
|
-
return;
|
|
325
|
-
const wasPooled = this.isWarm(entry);
|
|
326
|
-
entry.state = 'disposing';
|
|
327
|
-
this.entries.delete(entry.id);
|
|
328
|
-
if (wasPooled && opts.destroyIfPooled)
|
|
329
|
-
this.destroyWindow(entry.win);
|
|
330
|
-
// Refill ONLY on crash recovery (onGone). A graceful `'closed'` on a pooled
|
|
331
|
-
// window happens exclusively at app/project teardown (the windows are hidden;
|
|
332
|
-
// nothing else closes them) — refilling there would `new BrowserWindow` while
|
|
333
|
-
// Electron is trying to quit, recreating windows faster than they close and
|
|
334
|
-
// hanging the shutdown. So `refill` is opt-in per caller.
|
|
335
|
-
if (!opts.refill || this.disposed || !this.currentSpec || !wasPooled)
|
|
336
|
-
return;
|
|
337
|
-
while (this.warmCount() < this.targetSize) {
|
|
338
|
-
void this.warmOne(this.currentSpec);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/** A pooled window's render process died — reclaim + refill (destroy the husk). */
|
|
342
|
-
onGone(entry) {
|
|
343
|
-
this.reclaim(entry, { destroyIfPooled: true, refill: true });
|
|
344
|
-
}
|
|
345
|
-
disposeEntry(entry) {
|
|
346
|
-
entry.state = 'disposing';
|
|
347
|
-
this.entries.delete(entry.id);
|
|
348
|
-
this.destroyWindow(entry.win);
|
|
349
|
-
}
|
|
350
|
-
destroyWindow(win) {
|
|
351
|
-
try {
|
|
352
|
-
if (!win || (typeof win.isDestroyed === 'function' && win.isDestroyed()))
|
|
353
|
-
return;
|
|
354
|
-
if (typeof win.destroy === 'function')
|
|
355
|
-
win.destroy();
|
|
356
|
-
else if (typeof win.close === 'function')
|
|
357
|
-
win.close();
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
// A window already gone (e.g. crashed process) is fine to ignore.
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
1
|
+
export * from '@dimina-kit/electron-runtime/main/service-host-pool';
|
|
364
2
|
//# sourceMappingURL=pool.js.map
|
|
@@ -1,43 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export interface SimulatorApiRegistry {
|
|
3
|
-
register(name: string, handler: SimulatorApiHandler): () => void;
|
|
4
|
-
list(): string[];
|
|
5
|
-
has(name: string): boolean;
|
|
6
|
-
invoke(name: string, params: unknown): Promise<unknown>;
|
|
7
|
-
clear(): void;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Bridge request/response shapes shared by the simulator-side preload
|
|
11
|
-
* (`src/preload/runtime/custom-apis.ts`) and the two host-side dispatchers that
|
|
12
|
-
* service it (the trusted main-window renderer proxy was the original path; the
|
|
13
|
-
* native-host top-level WebContentsView dispatcher in the ViewManager is the
|
|
14
|
-
* current one). Kept here next to the registry so both stay in lockstep with
|
|
15
|
-
* the preload contract.
|
|
16
|
-
*/
|
|
17
|
-
export type CustomApiBridgeRequest = {
|
|
18
|
-
id: number;
|
|
19
|
-
op: 'list';
|
|
20
|
-
} | {
|
|
21
|
-
id: number;
|
|
22
|
-
op: 'invoke';
|
|
23
|
-
name: string;
|
|
24
|
-
params: unknown;
|
|
25
|
-
};
|
|
26
|
-
export type CustomApiBridgeResponse = {
|
|
27
|
-
id: number;
|
|
28
|
-
result: unknown;
|
|
29
|
-
} | {
|
|
30
|
-
id: number;
|
|
31
|
-
error: string;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Run a single custom-apis bridge request against the given registry and return
|
|
35
|
-
* the id-correlated response the preload's pending-map expects. Never throws:
|
|
36
|
-
* a handler rejection / malformed request becomes an `{ id, error }` response so
|
|
37
|
-
* the caller only has to ship it back over the bridge channel. The one shared
|
|
38
|
-
* implementation behind both the `SimulatorCustomApiChannel` IPC handlers and
|
|
39
|
-
* the native-host `ipc-message-host` dispatcher.
|
|
40
|
-
*/
|
|
41
|
-
export declare function handleCustomApiBridgeRequest(apis: Pick<SimulatorApiRegistry, 'list' | 'invoke'>, req: CustomApiBridgeRequest): Promise<CustomApiBridgeResponse>;
|
|
42
|
-
export declare function createSimulatorApiRegistry(): SimulatorApiRegistry;
|
|
1
|
+
export * from '@dimina-kit/electron-runtime/main/custom-apis';
|
|
43
2
|
//# sourceMappingURL=custom-apis.d.ts.map
|
|
@@ -1,47 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Run a single custom-apis bridge request against the given registry and return
|
|
3
|
-
* the id-correlated response the preload's pending-map expects. Never throws:
|
|
4
|
-
* a handler rejection / malformed request becomes an `{ id, error }` response so
|
|
5
|
-
* the caller only has to ship it back over the bridge channel. The one shared
|
|
6
|
-
* implementation behind both the `SimulatorCustomApiChannel` IPC handlers and
|
|
7
|
-
* the native-host `ipc-message-host` dispatcher.
|
|
8
|
-
*/
|
|
9
|
-
export async function handleCustomApiBridgeRequest(apis, req) {
|
|
10
|
-
try {
|
|
11
|
-
const result = req.op === 'list'
|
|
12
|
-
? apis.list()
|
|
13
|
-
: await apis.invoke(req.name, req.params);
|
|
14
|
-
return { id: req.id, result };
|
|
15
|
-
}
|
|
16
|
-
catch (err) {
|
|
17
|
-
return { id: req.id, error: err instanceof Error ? err.message : String(err) };
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
export function createSimulatorApiRegistry() {
|
|
21
|
-
const handlers = new Map();
|
|
22
|
-
return {
|
|
23
|
-
register(name, handler) {
|
|
24
|
-
handlers.set(name, handler);
|
|
25
|
-
return () => {
|
|
26
|
-
if (handlers.get(name) === handler)
|
|
27
|
-
handlers.delete(name);
|
|
28
|
-
};
|
|
29
|
-
},
|
|
30
|
-
list() {
|
|
31
|
-
return Array.from(handlers.keys());
|
|
32
|
-
},
|
|
33
|
-
has(name) {
|
|
34
|
-
return handlers.has(name);
|
|
35
|
-
},
|
|
36
|
-
async invoke(name, params) {
|
|
37
|
-
const handler = handlers.get(name);
|
|
38
|
-
if (!handler)
|
|
39
|
-
throw new Error(`Simulator API "${name}" is not registered`);
|
|
40
|
-
return await handler(params);
|
|
41
|
-
},
|
|
42
|
-
clear() {
|
|
43
|
-
handlers.clear();
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
1
|
+
export * from '@dimina-kit/electron-runtime/main/custom-apis';
|
|
47
2
|
//# sourceMappingURL=custom-apis.js.map
|
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* WeChat-style page-frame referer for HTTPS requests proxied from the simulator session.
|
|
3
|
-
* Format: https://servicewechat.com/{appId}/{version}/page-frame.html
|
|
4
|
-
*/
|
|
5
|
-
export declare function buildServicewechatPageFrameReferer(appId: string, version?: string): string;
|
|
6
|
-
/** Called when a project session is active so protocol.handle can force Referer. */
|
|
7
|
-
export declare function setSimulatorServicewechatReferer(appId: string, version?: string): void;
|
|
8
|
-
export declare function clearSimulatorServicewechatReferer(): void;
|
|
9
|
-
export declare function getSimulatorServicewechatReferer(): string | null;
|
|
1
|
+
export * from '@dimina-kit/electron-runtime/main/referer';
|
|
10
2
|
//# sourceMappingURL=referer.d.ts.map
|
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* WeChat-style page-frame referer for HTTPS requests proxied from the simulator session.
|
|
3
|
-
* Format: https://servicewechat.com/{appId}/{version}/page-frame.html
|
|
4
|
-
*/
|
|
5
|
-
const DEFAULT_VERSION = 'develop';
|
|
6
|
-
let cachedRefererUrl = null;
|
|
7
|
-
export function buildServicewechatPageFrameReferer(appId, version = DEFAULT_VERSION) {
|
|
8
|
-
return `https://servicewechat.com/${appId}/${version}/page-frame.html`;
|
|
9
|
-
}
|
|
10
|
-
/** Called when a project session is active so protocol.handle can force Referer. */
|
|
11
|
-
export function setSimulatorServicewechatReferer(appId, version) {
|
|
12
|
-
cachedRefererUrl = buildServicewechatPageFrameReferer(appId, version && version.length > 0 ? version : DEFAULT_VERSION);
|
|
13
|
-
}
|
|
14
|
-
export function clearSimulatorServicewechatReferer() {
|
|
15
|
-
cachedRefererUrl = null;
|
|
16
|
-
}
|
|
17
|
-
export function getSimulatorServicewechatReferer() {
|
|
18
|
-
return cachedRefererUrl;
|
|
19
|
-
}
|
|
1
|
+
export * from '@dimina-kit/electron-runtime/main/referer';
|
|
20
2
|
//# sourceMappingURL=referer.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WebContents } from 'electron';
|
|
2
|
+
import type { SimulatorUiExtensionHandle, SimulatorUiExtensionRegistration } from '../../../shared/simulator-ui.js';
|
|
3
|
+
export interface SimulatorUiExtensionRegistry {
|
|
4
|
+
register(registration: SimulatorUiExtensionRegistration): SimulatorUiExtensionHandle;
|
|
5
|
+
attach(webContents: WebContents): void;
|
|
6
|
+
detach(webContents?: WebContents): void;
|
|
7
|
+
clear(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface SimulatorUiExtensionRegistryOptions {
|
|
10
|
+
loadSource?: (scriptPath: string) => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSimulatorUiExtensionRegistry(options?: SimulatorUiExtensionRegistryOptions): SimulatorUiExtensionRegistry;
|
|
13
|
+
//# sourceMappingURL=ui-extensions.d.ts.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
const EXTENSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
|
+
export function createSimulatorUiExtensionRegistry(options = {}) {
|
|
6
|
+
const loadSource = options.loadSource ?? (scriptPath => readFile(scriptPath, 'utf8'));
|
|
7
|
+
const entries = new Map();
|
|
8
|
+
let active = null;
|
|
9
|
+
function assertRegistration(registration) {
|
|
10
|
+
if (!EXTENSION_ID_RE.test(registration.id)) {
|
|
11
|
+
throw new Error(`Invalid simulator UI extension id: "${registration.id}"`);
|
|
12
|
+
}
|
|
13
|
+
if (!isAbsolute(registration.rendererScriptPath)) {
|
|
14
|
+
throw new Error('Simulator UI extension rendererScriptPath must be absolute');
|
|
15
|
+
}
|
|
16
|
+
if (entries.has(registration.id)) {
|
|
17
|
+
throw new Error(`Simulator UI extension "${registration.id}" is already registered`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function activeExpression(method, args) {
|
|
21
|
+
return `globalThis.__diminaSimulatorUi.${method}(${args.map(arg => JSON.stringify(arg)).join(',')})`;
|
|
22
|
+
}
|
|
23
|
+
async function install(entry, target) {
|
|
24
|
+
const { id, rendererScriptPath } = entry.registration;
|
|
25
|
+
if (target.loaded.has(id))
|
|
26
|
+
return;
|
|
27
|
+
const pending = target.loading.get(id);
|
|
28
|
+
if (pending)
|
|
29
|
+
return await pending;
|
|
30
|
+
const generation = target.generation;
|
|
31
|
+
const task = (async () => {
|
|
32
|
+
// Read once per simulator document generation so downstream watch builds
|
|
33
|
+
// become visible after a hard reload without restarting the workbench.
|
|
34
|
+
const source = await loadSource(rendererScriptPath);
|
|
35
|
+
if (entries.get(id) !== entry
|
|
36
|
+
||
|
|
37
|
+
active !== target
|
|
38
|
+
|| target.webContents.isDestroyed()
|
|
39
|
+
|| !target.ready
|
|
40
|
+
|| target.generation !== generation) {
|
|
41
|
+
throw new Error('Simulator UI extension target is no longer available');
|
|
42
|
+
}
|
|
43
|
+
const sourceUrl = pathToFileURL(rendererScriptPath).href;
|
|
44
|
+
const wrapped = `(()=>{\n${source}\n;return undefined\n})()\n//# sourceURL=${sourceUrl}`;
|
|
45
|
+
await target.webContents.executeJavaScript(wrapped, true);
|
|
46
|
+
if (entries.get(id) !== entry
|
|
47
|
+
||
|
|
48
|
+
active !== target
|
|
49
|
+
|| target.webContents.isDestroyed()
|
|
50
|
+
|| !target.ready
|
|
51
|
+
|| target.generation !== generation) {
|
|
52
|
+
throw new Error('Simulator UI extension target was destroyed during installation');
|
|
53
|
+
}
|
|
54
|
+
const registered = await target.webContents.executeJavaScript(`Boolean(globalThis.__diminaSimulatorUi?.has(${JSON.stringify(id)}))`, true);
|
|
55
|
+
if (!registered) {
|
|
56
|
+
throw new Error(`Simulator UI extension "${id}" did not register its renderer implementation`);
|
|
57
|
+
}
|
|
58
|
+
target.loaded.add(id);
|
|
59
|
+
})().finally(() => {
|
|
60
|
+
if (target.loading.get(id) === task)
|
|
61
|
+
target.loading.delete(id);
|
|
62
|
+
});
|
|
63
|
+
target.loading.set(id, task);
|
|
64
|
+
return await task;
|
|
65
|
+
}
|
|
66
|
+
async function installAll(target) {
|
|
67
|
+
for (const entry of entries.values()) {
|
|
68
|
+
try {
|
|
69
|
+
await install(entry, target);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.warn(`[simulator-ui] failed to install "${entry.registration.id}":`, error instanceof Error ? error.message : String(error));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function uninstall(id, target = active) {
|
|
77
|
+
if (!target)
|
|
78
|
+
return;
|
|
79
|
+
target.loaded.delete(id);
|
|
80
|
+
target.loading.delete(id);
|
|
81
|
+
if (!target.ready || target.webContents.isDestroyed())
|
|
82
|
+
return;
|
|
83
|
+
void target.webContents.executeJavaScript(`globalThis.__diminaSimulatorUi?.unregister(${JSON.stringify(id)})`, true).catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
function detach(webContents) {
|
|
86
|
+
const target = active;
|
|
87
|
+
if (!target || (webContents && target.webContents !== webContents))
|
|
88
|
+
return;
|
|
89
|
+
active = null;
|
|
90
|
+
if (!target.webContents.isDestroyed()) {
|
|
91
|
+
target.webContents.removeListener('did-finish-load', target.onLoaded);
|
|
92
|
+
}
|
|
93
|
+
target.loaded.clear();
|
|
94
|
+
target.loading.clear();
|
|
95
|
+
}
|
|
96
|
+
function attach(webContents) {
|
|
97
|
+
detach();
|
|
98
|
+
const target = {
|
|
99
|
+
webContents,
|
|
100
|
+
ready: false,
|
|
101
|
+
generation: 0,
|
|
102
|
+
loaded: new Set(),
|
|
103
|
+
loading: new Map(),
|
|
104
|
+
onLoaded: () => { },
|
|
105
|
+
};
|
|
106
|
+
target.onLoaded = () => {
|
|
107
|
+
if (active !== target || webContents.isDestroyed())
|
|
108
|
+
return;
|
|
109
|
+
target.ready = true;
|
|
110
|
+
target.generation += 1;
|
|
111
|
+
target.loaded.clear();
|
|
112
|
+
target.loading.clear();
|
|
113
|
+
void installAll(target);
|
|
114
|
+
};
|
|
115
|
+
active = target;
|
|
116
|
+
webContents.on('did-finish-load', target.onLoaded);
|
|
117
|
+
}
|
|
118
|
+
function register(registration) {
|
|
119
|
+
assertRegistration(registration);
|
|
120
|
+
const stableRegistration = { ...registration };
|
|
121
|
+
const entry = {
|
|
122
|
+
registration: stableRegistration,
|
|
123
|
+
};
|
|
124
|
+
entries.set(stableRegistration.id, entry);
|
|
125
|
+
const target = active;
|
|
126
|
+
if (target?.ready) {
|
|
127
|
+
void install(entry, target).catch((error) => {
|
|
128
|
+
console.warn(`[simulator-ui] failed to install "${stableRegistration.id}":`, error instanceof Error ? error.message : String(error));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
let disposed = false;
|
|
132
|
+
return {
|
|
133
|
+
dispose() {
|
|
134
|
+
if (disposed)
|
|
135
|
+
return;
|
|
136
|
+
disposed = true;
|
|
137
|
+
if (entries.get(stableRegistration.id) !== entry)
|
|
138
|
+
return;
|
|
139
|
+
entries.delete(stableRegistration.id);
|
|
140
|
+
uninstall(stableRegistration.id);
|
|
141
|
+
},
|
|
142
|
+
async invoke(method, params) {
|
|
143
|
+
if (disposed || entries.get(stableRegistration.id) !== entry) {
|
|
144
|
+
throw new Error(`Simulator UI extension "${stableRegistration.id}" is disposed`);
|
|
145
|
+
}
|
|
146
|
+
const current = active;
|
|
147
|
+
if (!current || current.webContents.isDestroyed() || !current.ready) {
|
|
148
|
+
throw new Error('Dimina simulator UI is not ready');
|
|
149
|
+
}
|
|
150
|
+
await install(entry, current);
|
|
151
|
+
if (active !== current || current.webContents.isDestroyed()) {
|
|
152
|
+
throw new Error('Dimina simulator UI was replaced during invocation');
|
|
153
|
+
}
|
|
154
|
+
return await current.webContents.executeJavaScript(activeExpression('invoke', [stableRegistration.id, method, params ?? null]), true);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function clear() {
|
|
159
|
+
for (const id of entries.keys())
|
|
160
|
+
uninstall(id);
|
|
161
|
+
entries.clear();
|
|
162
|
+
detach();
|
|
163
|
+
}
|
|
164
|
+
return { register, attach, detach, clear };
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=ui-extensions.js.map
|