@dimina-kit/devtools 0.4.0-dev.20260718095333 → 0.4.0-dev.20260728063215
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/app/app.d.ts +10 -1
- package/dist/main/app/app.js +78 -2
- package/dist/main/app/lifecycle.d.ts +12 -1
- package/dist/main/app/lifecycle.js +27 -1
- package/dist/main/index.bundle.js +1263 -206
- package/dist/main/index.js +49 -1
- package/dist/main/ipc/bridge-router.d.ts +36 -0
- package/dist/main/ipc/bridge-router.js +56 -0
- package/dist/main/ipc/index.d.ts +1 -0
- package/dist/main/ipc/index.js +1 -0
- package/dist/main/ipc/internal-devtools.d.ts +15 -0
- package/dist/main/ipc/internal-devtools.js +9 -0
- package/dist/main/runtime/devtools-backend-before-quit.harness.d.ts +61 -0
- package/dist/main/runtime/devtools-backend-before-quit.harness.js +356 -0
- package/dist/main/runtime/devtools-backend.js +49 -4
- package/dist/main/services/compile-standby.js +4 -0
- package/dist/main/services/console-forward/cdp-inject.d.ts +18 -0
- package/dist/main/services/console-forward/cdp-inject.js +30 -0
- package/dist/main/services/console-forward/global-console-mirror.d.ts +33 -0
- package/dist/main/services/console-forward/global-console-mirror.js +71 -0
- package/dist/main/services/console-forward/global-diagnostics-mirror.d.ts +33 -0
- package/dist/main/services/console-forward/global-diagnostics-mirror.js +73 -0
- package/dist/main/services/console-forward/index.d.ts +15 -1
- package/dist/main/services/console-forward/index.js +42 -2
- package/dist/main/services/console-forward/internal-log.d.ts +11 -0
- package/dist/main/services/console-forward/internal-log.js +23 -0
- package/dist/main/services/console-forward/open-gated-relay.d.ts +67 -0
- package/dist/main/services/console-forward/open-gated-relay.js +55 -0
- package/dist/main/services/diagnostics/index.d.ts +10 -0
- package/dist/main/services/elements-forward/index.js +14 -96
- package/dist/main/services/mcp/server.js +24 -2
- package/dist/main/services/network-forward/frontend-dispatch.d.ts +64 -0
- package/dist/main/services/network-forward/frontend-dispatch.js +113 -8
- package/dist/main/services/network-forward/global-body-gate.d.ts +46 -0
- package/dist/main/services/network-forward/global-body-gate.js +93 -0
- package/dist/main/services/network-forward/global-mirror-test-fixtures.d.ts +48 -0
- package/dist/main/services/network-forward/global-mirror-test-fixtures.js +96 -0
- package/dist/main/services/network-forward/index.d.ts +31 -0
- package/dist/main/services/network-forward/index.js +256 -8
- package/dist/main/services/network-forward/user-facing.d.ts +21 -0
- package/dist/main/services/network-forward/user-facing.js +50 -0
- package/dist/main/services/projects/create-project-service.js +10 -3
- package/dist/main/services/update/github-release-checker.js +22 -3
- package/dist/main/services/update/update-manager.d.ts +4 -1
- package/dist/main/services/update/update-manager.js +30 -5
- package/dist/main/services/views/console-filter.d.ts +145 -0
- package/dist/main/services/views/console-filter.js +233 -0
- package/dist/main/services/views/frontend-bootstrap-gate.d.ts +59 -0
- package/dist/main/services/views/frontend-bootstrap-gate.js +117 -0
- package/dist/main/services/views/native-simulator-devtools-host.js +138 -30
- package/dist/main/services/views/view-manager-devtools-host-test-fixtures.d.ts +76 -0
- package/dist/main/services/views/view-manager-devtools-host-test-fixtures.js +182 -0
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/main/utils/theme.d.ts +10 -6
- package/dist/main/utils/theme.js +10 -6
- package/dist/main/windows/internal-devtools-window/index.d.ts +66 -0
- package/dist/main/windows/internal-devtools-window/index.js +160 -0
- package/dist/native-host/render/render.js +89 -73
- package/dist/native-host/service/service.js +2 -2
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +1 -1
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/renderer/assets/index-M5NDq_vi.js +49 -0
- package/dist/renderer/assets/{input-c2OcrrZy.js → input-dsgeTmVX.js} +2 -2
- package/dist/renderer/assets/ipc-transport-BgFdT9bT.js +9 -0
- package/dist/renderer/assets/{popover-kv2qspWz.js → popover-Disfu1cx.js} +2 -2
- package/dist/renderer/assets/{select-DKqzMtTh.js → select-DAw1hzkl.js} +2 -2
- package/dist/renderer/assets/{settings-dJNKTGr8.js → settings-M_8GFW2M.js} +2 -2
- package/dist/renderer/assets/settings-api-DFR5eDl_.js +2 -0
- package/dist/renderer/assets/{workbenchSettings-DQ_VP5-W.js → workbenchSettings-CxdXeoOu.js} +2 -2
- package/dist/renderer/entries/main/index.html +5 -5
- package/dist/renderer/entries/popover/index.html +4 -4
- package/dist/renderer/entries/settings/index.html +4 -4
- package/dist/renderer/entries/workbench-settings/index.html +3 -3
- package/dist/service-host/sync-impls/menu-button-geometry.js +15 -5
- package/dist/service-host/sync-impls/menu-button.js +5 -3
- package/dist/shared/ipc-channels.d.ts +3 -0
- package/dist/shared/ipc-channels.js +6 -0
- package/dist/simulator/assets/device-shell-Cs-rmP1f.js +2 -0
- package/dist/simulator/assets/device-shell-DOy53Y0s.css +1 -0
- package/dist/simulator/assets/{simulator-Cy8c-eA2.js → simulator-BG-5CADK.js} +3 -3
- package/dist/simulator/device-shell/menu-button-geometry.js +15 -5
- package/dist/simulator/simulator.html +1 -1
- package/package.json +7 -7
- package/dist/renderer/assets/index-U9F8MrtV.js +0 -49
- package/dist/renderer/assets/ipc-transport-CuDJ07aE.js +0 -9
- package/dist/renderer/assets/settings-api-qpXKDRXt.js +0 -2
- package/dist/simulator/assets/device-shell-C-HZY3bL.css +0 -1
- package/dist/simulator/assets/device-shell-Cu1ZPLfe.js +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runDevtoolsBootstrap, createDevtoolsRuntime } from '../app/app.js';
|
|
2
|
-
import { registerAppLifecycle } from '../app/lifecycle.js';
|
|
2
|
+
import { isAppQuitting, registerAppLifecycle } from '../app/lifecycle.js';
|
|
3
3
|
/**
|
|
4
4
|
* Adapts the devtools runtime into a {@link RuntimeBackend} the framework
|
|
5
5
|
* (`@dimina-kit/electron-deck`) orchestrates. The framework owns the process
|
|
@@ -21,6 +21,15 @@ import { registerAppLifecycle } from '../app/lifecycle.js';
|
|
|
21
21
|
export function createDevtoolsBackend(config = {}) {
|
|
22
22
|
// Hoisted so `onShutdown` can reach the assembled context (assigned by `assemble`).
|
|
23
23
|
let instance = null;
|
|
24
|
+
// `assemble`'s own in-flight promise: `instance` is now published EARLY (see
|
|
25
|
+
// below), before `createDevtoolsRuntime`'s async body — including the host's
|
|
26
|
+
// `config.onSetup(instance)` and everything after it — has finished running.
|
|
27
|
+
// `onShutdown` must wait for THIS to settle before disposing: disposing
|
|
28
|
+
// `instance.context.registry` while `createDevtoolsRuntime` is still adding
|
|
29
|
+
// entries to that same registry (e.g. the `updateChecker`/`setupMcp` wiring
|
|
30
|
+
// that runs after `onSetup`) hits `DisposableRegistry.add()`'s "cannot add
|
|
31
|
+
// to disposed registry" throw instead of a clean teardown.
|
|
32
|
+
let assembling = null;
|
|
24
33
|
return {
|
|
25
34
|
// The backend builds the devtools main window itself (framework skips its own).
|
|
26
35
|
// Its only construction-time needs are the host preload + `sandbox:false`
|
|
@@ -42,15 +51,51 @@ export function createDevtoolsBackend(config = {}) {
|
|
|
42
51
|
// Setup (post-whenReady): app lifecycle (window-all-closed → quit) + the
|
|
43
52
|
// full devtools assembly.
|
|
44
53
|
assemble: async () => {
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
// `onBeforeQuit` runs synchronously at Electron's `before-quit` — main
|
|
55
|
+
// loop still fully healthy, well before `will-quit`'s unawaited
|
|
56
|
+
// `shutdown()` and any window/WebContentsView destruction. Disposing
|
|
57
|
+
// the host-scoped views (host-toolbar's WebContentsView + its
|
|
58
|
+
// MessagePortMain) here, instead of leaving them to `onShutdown`'s
|
|
59
|
+
// best-effort async teardown, keeps them from surviving into
|
|
60
|
+
// Chromium's native shutdown sequence, where a late `'destroyed'`
|
|
61
|
+
// handler closing an already-torn-down MessagePort segfaults natively.
|
|
62
|
+
// Safe to run again from `onShutdown`'s `instance.dispose()` afterwards
|
|
63
|
+
// — `views.disposeAll()` and its constituents are dispose-idempotent.
|
|
64
|
+
registerAppLifecycle(() => instance?.context.views.disposeAll());
|
|
65
|
+
// `onInstanceCreated` (not the return value) is what assigns `instance`:
|
|
66
|
+
// `createDevtoolsRuntime` awaits the host's `config.onSetup(instance)`
|
|
67
|
+
// — which may run arbitrarily long and can itself load the host toolbar
|
|
68
|
+
// (a live MessagePort) — before returning. Waiting for the return value
|
|
69
|
+
// would leave `instance` null, and the before-quit hook above a no-op,
|
|
70
|
+
// for that entire window. Publishing from the callback closes it.
|
|
71
|
+
assembling = createDevtoolsRuntime(config, (created) => {
|
|
72
|
+
instance = created;
|
|
73
|
+
// Quit may have already started (before-quit already fired) before
|
|
74
|
+
// this instance existed, so the hook above ran with nothing to
|
|
75
|
+
// dispose. Self-heal: run the same teardown now that there is.
|
|
76
|
+
if (isAppQuitting())
|
|
77
|
+
instance.context.views.disposeAll();
|
|
78
|
+
}).then(() => { });
|
|
79
|
+
await assembling;
|
|
47
80
|
},
|
|
48
81
|
// Dispose the devtools context during the framework's deterministic shutdown
|
|
49
82
|
// (app.on('will-quit') → shutdown() → runShutdownCleanup(), awaited once).
|
|
50
83
|
// Without this, the compile session (a child process, torn down by
|
|
51
84
|
// closeProject) and the IPC registry would leak on exit. The framework awaits
|
|
52
85
|
// this hook, so teardown is no longer a best-effort before-quit reach-around.
|
|
53
|
-
|
|
86
|
+
//
|
|
87
|
+
// Waits for `assembling` first: `instance` can be non-null (published
|
|
88
|
+
// early, above) while `createDevtoolsRuntime`'s own async body — the
|
|
89
|
+
// host's `onSetup` and everything scheduled after it — is still running
|
|
90
|
+
// and still adding entries to `instance.context.registry`. Disposing
|
|
91
|
+
// that registry out from under an in-flight assembly throws instead of
|
|
92
|
+
// tearing down cleanly (`DisposableRegistry.add` rejects post-dispose).
|
|
93
|
+
// `.catch()` here: assembly failing is `assemble`'s problem to surface,
|
|
94
|
+
// not a reason to skip disposing whatever DID get constructed.
|
|
95
|
+
onShutdown: async () => {
|
|
96
|
+
await assembling?.catch(() => { });
|
|
97
|
+
await instance?.dispose();
|
|
98
|
+
},
|
|
54
99
|
};
|
|
55
100
|
}
|
|
56
101
|
//# sourceMappingURL=devtools-backend.js.map
|
|
@@ -21,6 +21,10 @@ export function setupCompileWorkerStandby(ctx, deps = {}) {
|
|
|
21
21
|
severity: EVENT_SEVERITY[ev.type] ?? 'info',
|
|
22
22
|
code: 'compile-standby',
|
|
23
23
|
message: detail,
|
|
24
|
+
// Devtools-tooling-only state, unrelated to the inspected mini-program
|
|
25
|
+
// — must never reach the right-panel (user-facing) Console panel; see
|
|
26
|
+
// console-forward/index.ts's audience gate and global-diagnostics-mirror.ts.
|
|
27
|
+
audience: 'internal',
|
|
24
28
|
});
|
|
25
29
|
}
|
|
26
30
|
catch {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WebContents } from 'electron';
|
|
2
|
+
import type { CdpSessionBroker } from '../cdp-session/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared CDP injection primitive for the global mirrors (console +
|
|
5
|
+
* diagnostics): evaluate `script` in `target`'s realm over the broker's
|
|
6
|
+
* debugger session — never `target.executeJavaScript`, whose internal RPC
|
|
7
|
+
* hangs forever on a wc that is simultaneously the inspected side of
|
|
8
|
+
* `setDevToolsWebContents` AND attached by an external CDP client (see
|
|
9
|
+
* global-console-mirror.ts for the full mechanism and the real-machine
|
|
10
|
+
* repro).
|
|
11
|
+
*
|
|
12
|
+
* Resolves true only on a clean evaluation. `exceptionDetails`, a rejected
|
|
13
|
+
* send, and an unacquirable session (destroyed wc / debugger exclusively
|
|
14
|
+
* held elsewhere) all report false — createOpenGatedRelay then leaves the
|
|
15
|
+
* entry retryable instead of marking it done.
|
|
16
|
+
*/
|
|
17
|
+
export declare function injectViaCdp(broker: CdpSessionBroker, target: WebContents, script: string, warnTag: string): boolean | Promise<boolean>;
|
|
18
|
+
//# sourceMappingURL=cdp-inject.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CDP injection primitive for the global mirrors (console +
|
|
3
|
+
* diagnostics): evaluate `script` in `target`'s realm over the broker's
|
|
4
|
+
* debugger session — never `target.executeJavaScript`, whose internal RPC
|
|
5
|
+
* hangs forever on a wc that is simultaneously the inspected side of
|
|
6
|
+
* `setDevToolsWebContents` AND attached by an external CDP client (see
|
|
7
|
+
* global-console-mirror.ts for the full mechanism and the real-machine
|
|
8
|
+
* repro).
|
|
9
|
+
*
|
|
10
|
+
* Resolves true only on a clean evaluation. `exceptionDetails`, a rejected
|
|
11
|
+
* send, and an unacquirable session (destroyed wc / debugger exclusively
|
|
12
|
+
* held elsewhere) all report false — createOpenGatedRelay then leaves the
|
|
13
|
+
* entry retryable instead of marking it done.
|
|
14
|
+
*/
|
|
15
|
+
export function injectViaCdp(broker, target, script, warnTag) {
|
|
16
|
+
const lease = broker.acquire(target);
|
|
17
|
+
if (!lease)
|
|
18
|
+
return false;
|
|
19
|
+
return lease.send('Runtime.evaluate', { expression: script }).then((result) => {
|
|
20
|
+
if (result?.exceptionDetails) {
|
|
21
|
+
console.warn(`[${warnTag}] Runtime.evaluate reported an exception, will retry on next reopen`);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}, (err) => {
|
|
26
|
+
console.warn(`[${warnTag}] injection failed, will retry on next reopen:`, err instanceof Error ? err.message : String(err));
|
|
27
|
+
return false;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=cdp-inject.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WebContents } from 'electron';
|
|
2
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
|
+
import type { ConsoleForwarder } from './index.js';
|
|
4
|
+
import type { CdpSessionBroker } from '../cdp-session/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Mirror EVERY guest console entry (both service + render layers,
|
|
7
|
+
* UNFILTERED — no isInternalLogMessage gating, see that module's doc for
|
|
8
|
+
* why this mirror deliberately skips it) into `target`'s own console, but
|
|
9
|
+
* ONLY while the standalone internal DevTools window is open.
|
|
10
|
+
*
|
|
11
|
+
* The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
|
|
12
|
+
* window just opened/rebuilt, null = just closed) rather than subscribing
|
|
13
|
+
* once for the whole app lifetime: each open re-subscribes to `forwarder`
|
|
14
|
+
* with `{replay:true}`, draining its CURRENT history buffer into `target`
|
|
15
|
+
* before continuing live, and each close disposes that subscription. This
|
|
16
|
+
* is what makes opening (or reopening) the window always show recent
|
|
17
|
+
* history — subscribing once at construction time (this module's earlier
|
|
18
|
+
* design) captured the replay burst before any window could possibly be
|
|
19
|
+
* open to receive it, silently losing it forever. `target` is the fixed
|
|
20
|
+
* INSPECTED side (mainWindow.webContents, per
|
|
21
|
+
* internal-devtools-window.ts's setDevToolsWebContents relationship) —
|
|
22
|
+
* never the hostWc the signal carries; injecting into the front-end host
|
|
23
|
+
* page would only ever reach that page's own invisible console.
|
|
24
|
+
*
|
|
25
|
+
* Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
|
|
26
|
+
* confirmed a naive replay-on-every-open double-injects everything already
|
|
27
|
+
* shown once, since Chromium's own console storage survives a DevTools
|
|
28
|
+
* close — see that module's doc comment).
|
|
29
|
+
*/
|
|
30
|
+
export declare function createGlobalConsoleMirror(forwarder: Pick<ConsoleForwarder, 'subscribe'>, target: WebContents, onHostChanged: (handler: (hostWc: WebContents | null) => void) => () => void, opts: {
|
|
31
|
+
broker: CdpSessionBroker;
|
|
32
|
+
}): Disposable;
|
|
33
|
+
//# sourceMappingURL=global-console-mirror.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
2
|
+
import { createOpenGatedRelay } from './open-gated-relay.js';
|
|
3
|
+
import { injectViaCdp } from './cdp-inject.js';
|
|
4
|
+
/** Console levels safe to re-emit; anything else maps to 'log' — mirrors
|
|
5
|
+
* console-forward/index.ts's FORWARDABLE_LEVELS. */
|
|
6
|
+
const FORWARDABLE_LEVELS = new Set(['log', 'warn', 'error', 'info', 'debug']);
|
|
7
|
+
/**
|
|
8
|
+
* Build the `executeJavaScript` source that re-emits one guest console entry
|
|
9
|
+
* into the target wc's own console, tagged with its source layer. Args ride
|
|
10
|
+
* as a JSON string and are re-parsed target-side — data, never code — same
|
|
11
|
+
* discipline as console-forward/index.ts's buildForwardScript.
|
|
12
|
+
*/
|
|
13
|
+
function buildMirrorScript(entry) {
|
|
14
|
+
const method = FORWARDABLE_LEVELS.has(entry.level ?? '') ? entry.level : 'log';
|
|
15
|
+
const tag = entry.source === 'render' ? '[render]' : '[service]';
|
|
16
|
+
const argsJson = JSON.stringify(entry.args ?? []);
|
|
17
|
+
return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}](${JSON.stringify(tag)},...a)}catch(_){}})()`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Mirror EVERY guest console entry (both service + render layers,
|
|
21
|
+
* UNFILTERED — no isInternalLogMessage gating, see that module's doc for
|
|
22
|
+
* why this mirror deliberately skips it) into `target`'s own console, but
|
|
23
|
+
* ONLY while the standalone internal DevTools window is open.
|
|
24
|
+
*
|
|
25
|
+
* The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
|
|
26
|
+
* window just opened/rebuilt, null = just closed) rather than subscribing
|
|
27
|
+
* once for the whole app lifetime: each open re-subscribes to `forwarder`
|
|
28
|
+
* with `{replay:true}`, draining its CURRENT history buffer into `target`
|
|
29
|
+
* before continuing live, and each close disposes that subscription. This
|
|
30
|
+
* is what makes opening (or reopening) the window always show recent
|
|
31
|
+
* history — subscribing once at construction time (this module's earlier
|
|
32
|
+
* design) captured the replay burst before any window could possibly be
|
|
33
|
+
* open to receive it, silently losing it forever. `target` is the fixed
|
|
34
|
+
* INSPECTED side (mainWindow.webContents, per
|
|
35
|
+
* internal-devtools-window.ts's setDevToolsWebContents relationship) —
|
|
36
|
+
* never the hostWc the signal carries; injecting into the front-end host
|
|
37
|
+
* page would only ever reach that page's own invisible console.
|
|
38
|
+
*
|
|
39
|
+
* Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
|
|
40
|
+
* confirmed a naive replay-on-every-open double-injects everything already
|
|
41
|
+
* shown once, since Chromium's own console storage survives a DevTools
|
|
42
|
+
* close — see that module's doc comment).
|
|
43
|
+
*/
|
|
44
|
+
export function createGlobalConsoleMirror(forwarder, target, onHostChanged, opts) {
|
|
45
|
+
function inject(entry) {
|
|
46
|
+
if (target.isDestroyed())
|
|
47
|
+
return false;
|
|
48
|
+
if (!isFrontendSettled(target))
|
|
49
|
+
return false;
|
|
50
|
+
let script;
|
|
51
|
+
try {
|
|
52
|
+
script = buildMirrorScript(entry);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// Transport is CDP `Runtime.evaluate` over the shared debugger-session
|
|
58
|
+
// broker — NEVER `target.executeJavaScript`: Electron's internal
|
|
59
|
+
// ExecuteJavaScript RPC hangs FOREVER on a wc that is simultaneously the
|
|
60
|
+
// inspected side of `setDevToolsWebContents` (the standalone debug
|
|
61
|
+
// window) AND attached by an external CDP client (remote-debugging-port,
|
|
62
|
+
// which bootstrap's setupCdpPort enables by default when unpackaged and
|
|
63
|
+
// whenever cdp/mcp settings are on) — real-machine repro. The debugger
|
|
64
|
+
// channel is unaffected. Report the real outcome (not fire-and-forget) —
|
|
65
|
+
// createOpenGatedRelay only marks an entry permanently "injected" on a
|
|
66
|
+
// confirmed `true`, and its inject timeout bounds any residual hang.
|
|
67
|
+
return injectViaCdp(opts.broker, target, script, 'global-console-mirror');
|
|
68
|
+
}
|
|
69
|
+
return createOpenGatedRelay(onHostChanged, (sink, opts2) => forwarder.subscribe(sink, opts2), inject);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=global-console-mirror.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WebContents } from 'electron';
|
|
2
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
|
+
import type { DiagnosticsBus } from '../diagnostics/index.js';
|
|
4
|
+
import type { CdpSessionBroker } from '../cdp-session/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Mirror EVERY diagnostic (both `audience:'user'` and `audience:'internal'`,
|
|
7
|
+
* UNFILTERED — the same "see everything" contract `createGlobalConsoleMirror`
|
|
8
|
+
* applies to guest console entries) into `target`'s own console, but ONLY
|
|
9
|
+
* while the standalone internal DevTools window is open.
|
|
10
|
+
*
|
|
11
|
+
* The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
|
|
12
|
+
* window just opened/rebuilt, null = just closed) rather than subscribing
|
|
13
|
+
* once for the whole app lifetime: each open re-subscribes to `diagnostics`
|
|
14
|
+
* with `{replay:true}`, draining its CURRENT buffer into `target` before
|
|
15
|
+
* continuing live, and each close disposes that subscription. This is what
|
|
16
|
+
* makes opening (or reopening) the window always show history — subscribing
|
|
17
|
+
* once at construction time (this module's earlier design) captured the
|
|
18
|
+
* replay burst before any window could possibly be open to receive it
|
|
19
|
+
* (construction happens at app boot; the window is user-opened, always
|
|
20
|
+
* later), silently losing it forever — the exact reported bug (e.g. the
|
|
21
|
+
* earliest compile-standby events at boot never showing up). `target` is the
|
|
22
|
+
* fixed INSPECTED side (mainWindow.webContents) — never the hostWc the
|
|
23
|
+
* signal carries.
|
|
24
|
+
*
|
|
25
|
+
* Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
|
|
26
|
+
* confirmed a naive replay-on-every-open double-injects everything already
|
|
27
|
+
* shown once, since Chromium's own console storage survives a DevTools
|
|
28
|
+
* close — see that module's doc comment).
|
|
29
|
+
*/
|
|
30
|
+
export declare function createGlobalDiagnosticsMirror(diagnostics: Pick<DiagnosticsBus, 'subscribe'>, target: WebContents, onHostChanged: (handler: (hostWc: WebContents | null) => void) => () => void, opts: {
|
|
31
|
+
broker: CdpSessionBroker;
|
|
32
|
+
}): Disposable;
|
|
33
|
+
//# sourceMappingURL=global-diagnostics-mirror.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
2
|
+
import { createOpenGatedRelay } from './open-gated-relay.js';
|
|
3
|
+
import { injectViaCdp } from './cdp-inject.js';
|
|
4
|
+
/** `Diagnostic.severity` → the literal `console.<method>` call — same mapping
|
|
5
|
+
* console-forward/index.ts's DIAGNOSTIC_CONSOLE_CALL uses (kept local since
|
|
6
|
+
* that constant isn't exported; both must stay in sync with `Diagnostic`). */
|
|
7
|
+
const DIAGNOSTIC_CONSOLE_CALL = {
|
|
8
|
+
error: 'console.error',
|
|
9
|
+
warn: 'console.warn',
|
|
10
|
+
info: 'console.info',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Build the `executeJavaScript` source that injects one diagnostic into the
|
|
14
|
+
* target wc's own console, prefixed `[dimina-kit] ` — same shape as
|
|
15
|
+
* console-forward/index.ts's `buildDiagnosticScript`, minus that function's
|
|
16
|
+
* `RENDER_FORWARD_SOURCE_URL` loop-safety sentinel: that sentinel exists to
|
|
17
|
+
* stop the service-host's OWN CDP capture from re-broadcasting the injected
|
|
18
|
+
* line, and nothing here re-captures `mainWindow.webContents`'s console.
|
|
19
|
+
*/
|
|
20
|
+
function buildMirrorScript(severity, message) {
|
|
21
|
+
const call = DIAGNOSTIC_CONSOLE_CALL[severity];
|
|
22
|
+
const argsJson = JSON.stringify([`[dimina-kit] ${message}`]);
|
|
23
|
+
return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});${call}(...a)}catch(_){}})()`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mirror EVERY diagnostic (both `audience:'user'` and `audience:'internal'`,
|
|
27
|
+
* UNFILTERED — the same "see everything" contract `createGlobalConsoleMirror`
|
|
28
|
+
* applies to guest console entries) into `target`'s own console, but ONLY
|
|
29
|
+
* while the standalone internal DevTools window is open.
|
|
30
|
+
*
|
|
31
|
+
* The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
|
|
32
|
+
* window just opened/rebuilt, null = just closed) rather than subscribing
|
|
33
|
+
* once for the whole app lifetime: each open re-subscribes to `diagnostics`
|
|
34
|
+
* with `{replay:true}`, draining its CURRENT buffer into `target` before
|
|
35
|
+
* continuing live, and each close disposes that subscription. This is what
|
|
36
|
+
* makes opening (or reopening) the window always show history — subscribing
|
|
37
|
+
* once at construction time (this module's earlier design) captured the
|
|
38
|
+
* replay burst before any window could possibly be open to receive it
|
|
39
|
+
* (construction happens at app boot; the window is user-opened, always
|
|
40
|
+
* later), silently losing it forever — the exact reported bug (e.g. the
|
|
41
|
+
* earliest compile-standby events at boot never showing up). `target` is the
|
|
42
|
+
* fixed INSPECTED side (mainWindow.webContents) — never the hostWc the
|
|
43
|
+
* signal carries.
|
|
44
|
+
*
|
|
45
|
+
* Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
|
|
46
|
+
* confirmed a naive replay-on-every-open double-injects everything already
|
|
47
|
+
* shown once, since Chromium's own console storage survives a DevTools
|
|
48
|
+
* close — see that module's doc comment).
|
|
49
|
+
*/
|
|
50
|
+
export function createGlobalDiagnosticsMirror(diagnostics, target, onHostChanged, opts) {
|
|
51
|
+
function inject(d) {
|
|
52
|
+
if (target.isDestroyed())
|
|
53
|
+
return false;
|
|
54
|
+
if (!isFrontendSettled(target))
|
|
55
|
+
return false;
|
|
56
|
+
let script;
|
|
57
|
+
try {
|
|
58
|
+
script = buildMirrorScript(d.severity, d.message);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
// Transport is CDP `Runtime.evaluate` over the shared debugger-session
|
|
64
|
+
// broker — never `target.executeJavaScript`, whose internal RPC hangs
|
|
65
|
+
// forever under the setDevToolsWebContents + external-CDP-client double
|
|
66
|
+
// attach (see cdp-inject.ts / global-console-mirror.ts; same target,
|
|
67
|
+
// same failure). Report the real outcome — createOpenGatedRelay only
|
|
68
|
+
// marks an entry permanently "injected" on a confirmed `true`.
|
|
69
|
+
return injectViaCdp(opts.broker, target, script, 'global-diagnostics-mirror');
|
|
70
|
+
}
|
|
71
|
+
return createOpenGatedRelay(onHostChanged, (sink, opts2) => diagnostics.subscribe(sink, opts2), inject);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=global-diagnostics-mirror.js.map
|
|
@@ -30,8 +30,22 @@ export interface ConsoleForwarder extends Disposable {
|
|
|
30
30
|
* Register an external sink (e.g. automation WS broadcast). Returns a
|
|
31
31
|
* Disposable that unregisters it. Sinks see EVERY entry (both layers) — the
|
|
32
32
|
* render→service forward is internal and not exposed as a sink.
|
|
33
|
+
*
|
|
34
|
+
* A bounded history buffer (200 entries, oldest dropped first — mirrors
|
|
35
|
+
* `DiagnosticsBus`'s `DEFAULT_BUFFER_CAP` ring-buffer style) records every
|
|
36
|
+
* entry regardless of whether anyone is subscribed. `opts.replay` (default
|
|
37
|
+
* `false`, UNLIKE `DiagnosticsBus` whose default is `true`) opts a new
|
|
38
|
+
* subscriber into draining that buffer, in order, before receiving live
|
|
39
|
+
* entries. Default stays non-replaying because the existing automation
|
|
40
|
+
* subscriber (`services/automation/index.ts`) wants only entries from the
|
|
41
|
+
* moment it subscribes — a history dump on an existing WS connection would
|
|
42
|
+
* be a surprise duplicate broadcast. `{replay:true}` exists for the global
|
|
43
|
+
* console mirror, whose whole point is to catch up a standalone debug
|
|
44
|
+
* window opened well after boot.
|
|
33
45
|
*/
|
|
34
|
-
subscribe(sink: ConsoleSink
|
|
46
|
+
subscribe(sink: ConsoleSink, opts?: {
|
|
47
|
+
replay?: boolean;
|
|
48
|
+
}): Disposable;
|
|
35
49
|
/**
|
|
36
50
|
* Flush any diagnostic queued for `appSessionId` (plus the global,
|
|
37
51
|
* session-less queue) into that session's now-ready service-host wc.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
|
|
2
2
|
import { RENDER_FORWARD_SOURCE_URL } from '../service-console/console-api.js';
|
|
3
|
+
import { isInternalLogMessage } from './internal-log.js';
|
|
3
4
|
/** Console levels we re-emit into the service host. Anything else maps to 'log'. */
|
|
4
5
|
const FORWARDABLE_LEVELS = new Set(['log', 'warn', 'error', 'info', 'debug']);
|
|
5
6
|
/**
|
|
@@ -46,6 +47,12 @@ function buildDiagnosticScript(severity, message) {
|
|
|
46
47
|
export function createConsoleForwarder(bridge, diagnostics) {
|
|
47
48
|
const sinks = new Set();
|
|
48
49
|
const registry = new DisposableRegistry();
|
|
50
|
+
// Bounded history ring buffer backing `subscribe(sink, {replay:true})` —
|
|
51
|
+
// see the interface doc comment above. Every entry is recorded regardless
|
|
52
|
+
// of whether anyone is subscribed, so a replaying subscriber that arrives
|
|
53
|
+
// long after boot (the standalone global debug window) still catches up.
|
|
54
|
+
const CONSOLE_BUFFER_CAP = 200;
|
|
55
|
+
const buffer = [];
|
|
49
56
|
// Diagnostics queued because no live service-host wc could be resolved yet,
|
|
50
57
|
// bucketed by the owning appSessionId; diagnostics with no appSessionId land
|
|
51
58
|
// in `pendingGlobal` instead. `notifyServiceHostReady` drains both into the
|
|
@@ -73,6 +80,16 @@ export function createConsoleForwarder(bridge, diagnostics) {
|
|
|
73
80
|
* missing/destroyed/not-yet-ready host is a normal state, not an error.
|
|
74
81
|
*/
|
|
75
82
|
function handleDiagnostic(d) {
|
|
83
|
+
// Internal-audience diagnostics (e.g. compile-standby's warm-pool
|
|
84
|
+
// lifecycle) are devtools-tooling-only state — never inject them into a
|
|
85
|
+
// project's service host (the right-panel CDP is natively attached
|
|
86
|
+
// there), and never queue them either: queuing exists so a diagnostic
|
|
87
|
+
// survives until ITS session's host becomes resolvable, which is
|
|
88
|
+
// meaningless for something that must never reach any service host.
|
|
89
|
+
// (See global-diagnostics-mirror.ts for where these actually surface —
|
|
90
|
+
// the independent, unfiltered "debug the whole app" panel.)
|
|
91
|
+
if (d.audience === 'internal')
|
|
92
|
+
return;
|
|
76
93
|
if (d.appSessionId) {
|
|
77
94
|
const wc = readySessions.has(d.appSessionId) && bridge.getServiceWcForBridge
|
|
78
95
|
? bridge.getServiceWcForBridge(d.appSessionId)
|
|
@@ -105,9 +122,20 @@ export function createConsoleForwarder(bridge, diagnostics) {
|
|
|
105
122
|
* page (multi-app safe), falling back to the active app's service host when
|
|
106
123
|
* the bridgeId is unknown — that's the host the embedded DevTools inspects.
|
|
107
124
|
* No-op on a destroyed/missing host so we never write to a torn-down wc (pool
|
|
108
|
-
* reuse / session swap can swap the host out under us).
|
|
125
|
+
* reuse / session swap can swap the host out under us). Skips dimina
|
|
126
|
+
* framework-internal render-layer log lines (`isInternalLogMessage`) —
|
|
127
|
+
* those must never reach the project's right-panel console at all (the
|
|
128
|
+
* global console mirror still sees them via the `sinks` broadcast in
|
|
129
|
+
* `emit()`, which runs before this function). This is the source-level fix
|
|
130
|
+
* for the render half of the noise console-filter.ts's front-end text
|
|
131
|
+
* filter used to paper over; the service-layer half (`[service]` lines,
|
|
132
|
+
* native CDP-observed, no interception point available — see
|
|
133
|
+
* service-host/preload.cjs's "console.* is deliberately NOT patched"
|
|
134
|
+
* comment) still needs that filter.
|
|
109
135
|
*/
|
|
110
136
|
function forwardRenderToServiceHost(entry) {
|
|
137
|
+
if (isInternalLogMessage(entry))
|
|
138
|
+
return;
|
|
111
139
|
let wc = null;
|
|
112
140
|
if (entry.bridgeId && bridge.getServiceWcForBridge) {
|
|
113
141
|
wc = bridge.getServiceWcForBridge(entry.bridgeId);
|
|
@@ -135,6 +163,9 @@ export function createConsoleForwarder(bridge, diagnostics) {
|
|
|
135
163
|
// (render only — see loop-safety invariant in the module header).
|
|
136
164
|
emit(raw) {
|
|
137
165
|
const entry = (raw ?? {});
|
|
166
|
+
buffer.push(entry);
|
|
167
|
+
if (buffer.length > CONSOLE_BUFFER_CAP)
|
|
168
|
+
buffer.shift();
|
|
138
169
|
for (const sink of sinks) {
|
|
139
170
|
try {
|
|
140
171
|
sink(entry);
|
|
@@ -147,7 +178,15 @@ export function createConsoleForwarder(bridge, diagnostics) {
|
|
|
147
178
|
if (entry.source === 'render')
|
|
148
179
|
forwardRenderToServiceHost(entry);
|
|
149
180
|
},
|
|
150
|
-
subscribe(sink) {
|
|
181
|
+
subscribe(sink, opts) {
|
|
182
|
+
if (opts?.replay) {
|
|
183
|
+
for (const entry of buffer) {
|
|
184
|
+
try {
|
|
185
|
+
sink(entry);
|
|
186
|
+
}
|
|
187
|
+
catch { /* isolate a replay throw same as a live one */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
151
190
|
sinks.add(sink);
|
|
152
191
|
return registry.add(toDisposable(() => { sinks.delete(sink); }));
|
|
153
192
|
},
|
|
@@ -176,6 +215,7 @@ export function createConsoleForwarder(bridge, diagnostics) {
|
|
|
176
215
|
},
|
|
177
216
|
dispose() {
|
|
178
217
|
sinks.clear();
|
|
218
|
+
buffer.length = 0;
|
|
179
219
|
pendingBySession.clear();
|
|
180
220
|
pendingGlobal.length = 0;
|
|
181
221
|
readySessions.clear();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GuestConsoleEntry } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Judges whether a guest console entry is dimina framework-internal output
|
|
4
|
+
* (a bridge/lifecycle log the framework itself prints) rather than the
|
|
5
|
+
* mini-app author's own business `console.*` call. Only the FIRST arg is
|
|
6
|
+
* inspected — framework logs always lead with the literal tag; a
|
|
7
|
+
* business log that happens to mention the tag later in its args must not
|
|
8
|
+
* be swept up.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isInternalLogMessage(entry: GuestConsoleEntry): boolean;
|
|
11
|
+
//# sourceMappingURL=internal-log.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** dimina framework internal-log first-arg prefixes, from the framework
|
|
2
|
+
* source literals (dimina/fe/packages/service/src/core/message.js:26 and
|
|
3
|
+
* siblings use `'[service]'`; dimina/fe/packages/render/src/core/message.js:18
|
|
4
|
+
* and siblings use `'[system]'`). */
|
|
5
|
+
const SERVICE_PREFIX = '[service]';
|
|
6
|
+
const RENDER_PREFIX = '[system]';
|
|
7
|
+
/**
|
|
8
|
+
* Judges whether a guest console entry is dimina framework-internal output
|
|
9
|
+
* (a bridge/lifecycle log the framework itself prints) rather than the
|
|
10
|
+
* mini-app author's own business `console.*` call. Only the FIRST arg is
|
|
11
|
+
* inspected — framework logs always lead with the literal tag; a
|
|
12
|
+
* business log that happens to mention the tag later in its args must not
|
|
13
|
+
* be swept up.
|
|
14
|
+
*/
|
|
15
|
+
export function isInternalLogMessage(entry) {
|
|
16
|
+
const first = entry.args?.[0];
|
|
17
|
+
if (typeof first !== 'string')
|
|
18
|
+
return false;
|
|
19
|
+
if (first === RENDER_PREFIX)
|
|
20
|
+
return true;
|
|
21
|
+
return first === SERVICE_PREFIX || first.startsWith(`${SERVICE_PREFIX} `);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=internal-log.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
2
|
+
/**
|
|
3
|
+
* Generic open/close-gated replay-subscription primitive shared by
|
|
4
|
+
* `createGlobalConsoleMirror` and `createGlobalDiagnosticsMirror` — both
|
|
5
|
+
* gate their subscription to a buffered+replayable source
|
|
6
|
+
* (`ConsoleForwarder`/`DiagnosticsBus`) on whether the standalone "debug the
|
|
7
|
+
* whole Electron app" window is currently open (`onHostChanged`): each open
|
|
8
|
+
* re-subscribes with `{replay:true}` so history is never lost even if the
|
|
9
|
+
* window opens long after boot, and each close disposes that subscription.
|
|
10
|
+
*
|
|
11
|
+
* ── Dedup (the e2e-confirmed bug this fixes) ────────────────────────────────
|
|
12
|
+
* Naively re-subscribing with `{replay:true}` on every reopen double-injects
|
|
13
|
+
* history: Chromium's own per-frame `ConsoleMessageStorage` is NOT cleared by
|
|
14
|
+
* closing DevTools (only by navigation), so it natively re-delivers entries
|
|
15
|
+
* already shown during a PREVIOUS open to the freshly-attached front-end —
|
|
16
|
+
* and if this relay ALSO blindly replays and re-injects the same buffered
|
|
17
|
+
* objects, every entry shown once ends up displayed twice on reopen, the
|
|
18
|
+
* duplicate carrying a fresh (wrong) "just reopened" timestamp instead of
|
|
19
|
+
* when it actually happened.
|
|
20
|
+
*
|
|
21
|
+
* Fix: `state` is a `WeakMap` keyed by OBJECT REFERENCE (never content — two
|
|
22
|
+
* content-identical-but-distinct entries must each still get injected once)
|
|
23
|
+
* that persists across the relay's ENTIRE lifetime, not reset by open/close.
|
|
24
|
+
* A replay that re-delivers an already-CONFIRMED-injected object is skipped;
|
|
25
|
+
* only entries never physically (and successfully) passed to `inject()`
|
|
26
|
+
* before (e.g. ones that arrived while the window was closed) actually call
|
|
27
|
+
* `inject()`. Chromium's own native re-delivery is left to show the
|
|
28
|
+
* once-injected ones — that is what makes reopening not double them up. This
|
|
29
|
+
* also composes correctly with the source's own bounded ring buffer: an
|
|
30
|
+
* evicted entry is simply absent from the next replay batch entirely, so it
|
|
31
|
+
* can never be double-counted regardless of what `state` holds.
|
|
32
|
+
*
|
|
33
|
+
* ── inject() is fallible — only a CONFIRMED success may ever mark "done" ───
|
|
34
|
+
* `inject()` wraps an async `executeJavaScript` against a possibly-destroyed
|
|
35
|
+
* or not-yet-settled target, so it reports whether the entry was ACTUALLY
|
|
36
|
+
* delivered via `boolean | Promise<boolean>`. Marking an entry "injected"
|
|
37
|
+
* before that outcome is known (the original bug here) means a single
|
|
38
|
+
* transient failure — target destroyed, front-end not settled, a rejected
|
|
39
|
+
* `executeJavaScript` — permanently black-holes that entry: every future
|
|
40
|
+
* replay skips it forever, even though it was never actually shown anywhere
|
|
41
|
+
* (violates this repo's "state must be marked at the moment the fact
|
|
42
|
+
* actually happens" principle — this was marking INTENT, not the fact). So
|
|
43
|
+
* `state` tracks three phases per entry: absent (never attempted, or a prior
|
|
44
|
+
* attempt failed and was cleared — eligible for `inject()`), `'pending'`
|
|
45
|
+
* (an `inject()` call is in flight — a concurrent replay of the SAME entry
|
|
46
|
+
* must not trigger a second concurrent call), `'done'` (confirmed success —
|
|
47
|
+
* permanently skipped from here on). A `false` result or a rejection resets
|
|
48
|
+
* the entry back to absent, so the next replay (e.g. the next window reopen)
|
|
49
|
+
* gets a real retry instead of a silent, permanent loss.
|
|
50
|
+
*/
|
|
51
|
+
export interface OpenGatedRelayOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Bounds how long ONE in-flight `inject()` attempt may hold an entry
|
|
54
|
+
* `'pending'`. An `inject()` promise that never settles (real incident:
|
|
55
|
+
* `executeJavaScript` against a wc that is simultaneously the inspected
|
|
56
|
+
* side of `setDevToolsWebContents` AND attached by an external CDP client
|
|
57
|
+
* hangs forever) would otherwise black-hole the entry permanently — never
|
|
58
|
+
* shown, never retried. Past this bound the entry becomes retryable again
|
|
59
|
+
* on the next replay. The timed-out attempt's promise may still settle
|
|
60
|
+
* LATE; its settlement is generation-stamped and ignored when stale.
|
|
61
|
+
*/
|
|
62
|
+
injectTimeoutMs?: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function createOpenGatedRelay<TEntry extends object, THost = unknown>(onHostChanged: (handler: (host: THost | null) => void) => () => void, subscribe: (sink: (entry: TEntry) => void, opts: {
|
|
65
|
+
replay: true;
|
|
66
|
+
}) => Disposable, inject: (entry: TEntry) => boolean | Promise<boolean>, opts?: OpenGatedRelayOptions): Disposable;
|
|
67
|
+
//# sourceMappingURL=open-gated-relay.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_INJECT_TIMEOUT_MS = 10_000;
|
|
2
|
+
export function createOpenGatedRelay(onHostChanged, subscribe, inject, opts) {
|
|
3
|
+
const injectTimeoutMs = opts?.injectTimeoutMs ?? DEFAULT_INJECT_TIMEOUT_MS;
|
|
4
|
+
// Generation stamp per attempt: only the attempt matching the entry's
|
|
5
|
+
// CURRENT pending generation may act on settling — a stale (timed-out,
|
|
6
|
+
// superseded) attempt's late result must neither mark the entry done nor
|
|
7
|
+
// disturb a newer attempt's pending status.
|
|
8
|
+
const state = new WeakMap();
|
|
9
|
+
let generationSeq = 0;
|
|
10
|
+
const inFlightTimers = new Set();
|
|
11
|
+
let live = null;
|
|
12
|
+
function deliver(entry) {
|
|
13
|
+
if (state.has(entry))
|
|
14
|
+
return;
|
|
15
|
+
const generation = ++generationSeq;
|
|
16
|
+
state.set(entry, { phase: 'pending', generation });
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
inFlightTimers.delete(timer);
|
|
19
|
+
const cur = state.get(entry);
|
|
20
|
+
if (cur && cur.phase === 'pending' && cur.generation === generation)
|
|
21
|
+
state.delete(entry);
|
|
22
|
+
}, injectTimeoutMs);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
inFlightTimers.add(timer);
|
|
25
|
+
const settle = (ok) => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
inFlightTimers.delete(timer);
|
|
28
|
+
const cur = state.get(entry);
|
|
29
|
+
if (!cur || cur.phase !== 'pending' || cur.generation !== generation)
|
|
30
|
+
return;
|
|
31
|
+
if (ok)
|
|
32
|
+
state.set(entry, { phase: 'done', generation });
|
|
33
|
+
else
|
|
34
|
+
state.delete(entry);
|
|
35
|
+
};
|
|
36
|
+
Promise.resolve()
|
|
37
|
+
.then(() => inject(entry))
|
|
38
|
+
.then((ok) => settle(ok === true), () => settle(false));
|
|
39
|
+
}
|
|
40
|
+
const unregister = onHostChanged((host) => {
|
|
41
|
+
live?.dispose();
|
|
42
|
+
live = host !== null ? subscribe(deliver, { replay: true }) : null;
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
dispose: () => {
|
|
46
|
+
for (const timer of inFlightTimers)
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
inFlightTimers.clear();
|
|
49
|
+
live?.dispose();
|
|
50
|
+
live = null;
|
|
51
|
+
unregister();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=open-gated-relay.js.map
|