Bläddra i källkod

fix(inspector): satisfy CI checks

imccyu 1 månad sedan
förälder
incheckning
b1748f0c55

+ 2 - 2
packages/experimental/inspector/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/experimental/inspector/README.md
-README.md: dcebe9527de1b909fe1efc9f73977735ad374a95
-README.zh.md: 9ca43b0420f22969b6bd060bf9546eb094c4fbc0
+README.md: 86357b3a91763571ffe0cf9eea389b2b21c84d77
+README.zh.md: ce030fa3873f4cd488855d5c963a27eac0df9c8e

+ 45 - 1
packages/experimental/inspector/README.md

@@ -1,11 +1,33 @@
+---
+description: "Experimental Chrome DevTools inspection for Host and browser Client Cordis runtimes, including Console evaluation, Sources, Network capture, Elements trees, and a CDP-independent query API."
+kind: "package-reference"
+---
+
 # @deepseek-ai/dsh-experimental-inspector
 
 English | [中文](README.zh.md)
 
-Experimental Client/Host Cordis plugin that exposes one Chrome DevTools Protocol target from a Node worker thread. The Host and browser Client publish versioned observations into the Worker; the Worker is the sole owner of CDP state and output.
+## Summary
+
+Use this experimental inspector to inspect one running dsh Host and its browser Clients in Chrome DevTools. It exposes Host and Client Console contexts, Host Sources and debugging, captured Host fetches, and a shared Cordis tree while keeping all CDP state in a Worker.
 
 The package is private and excluded from releases. The Worker never accesses live Cordis objects: the shared Host/Client collector projects them into validated snapshots before transport. Cordis also owns plugin composition, `ctx.inspector` registration, bootstrap injection, and disposal.
 
+## Table of Contents
+
+- [Runtime layout](#runtime-layout)
+- [Configuration](#configuration)
+- [Observation API](#observation-api)
+- [Cordis tree inspection](#cordis-tree-inspection)
+- [Host fetch capture](#host-fetch-capture)
+- [Security](#security)
+- [Model Experience](#model-experience)
+- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
+- [Dev Note](#dev-note)
+
+-----
+
+<a id="runtime-layout"></a>
 ## Runtime layout
 
 The Host plugin starts the Worker and connects a dedicated `MessagePort`. The Client plugin reads the injected `globalThis.__DSH_INSPECTOR__` bootstrap and opens a separate authenticated WebSocket directly to the Worker. Chrome DevTools connects to the Worker's CDP WebSocket. A private `node:inspector.Session` per DevTools connection attaches from the Worker to the Host main thread, so Host Console evaluation, Sources, breakpoints, and resume remain available while Host JavaScript is paused.
@@ -18,6 +40,7 @@ Client sources declare typed Runtime, Console, and read-only Sources capabilitie
 
 Both plugin faces run the same browser-safe Cordis collector. It converts reachable Context and Fiber objects into a versioned `CordisTreeSnapshot`; the Worker stores that CDP-independent representation and projects each Host or Client source into the Elements panel.
 
+<a id="configuration"></a>
 ## Configuration
 
 The Host plugin injects `webServer` and accepts these fields:
@@ -49,8 +72,11 @@ The Host plugin injects `webServer` and accepts these fields:
 | `maxCordisNodes` | `2048` | Context and Fiber nodes admitted from one realm snapshot before truncation |
 | `maxDisconnectedCordisTrees` | `8` | Last disconnected realm trees retained as non-live snapshots |
 
+The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-inspector) is the exhaustive source for accepted fields and their declarations.
+
 The Host logs a `devtools://` URL after the Worker listens. The same Worker serves `/json`, `/json/list`, `/json/version`, the target WebSocket under `/devtools/page/<id>`, and the Client source at `/ingest`.
 
+<a id="observation-api"></a>
 ## Observation API
 
 Both plugin faces provide the same service:
@@ -69,6 +95,7 @@ await ctx.inspector.cordis.getTree()
 
 Publishing validates lossless JSON and schedules delivery without waiting for the Worker. Each source has a bounded queue. Overflow is reported as a sequence gap and never delays the observed application operation. `cordis.getTree()` reads the Worker's latest detached semantic snapshot without creating a CDP session or enabling Runtime, Debugger, or Sources.
 
+<a id="cordis-tree-inspection"></a>
 ## Cordis tree inspection
 
 The Elements document has fixed `<host>` and `<clients>` containers. `<host>` contains the Host root Context; `<clients>` contains one `<client>` per Client source, and each `<client>` contains that realm's root Context. The Cordis root Fiber is omitted. Every other Fiber is a child of `fiber.parent`, owns exactly one Context child for `fiber.ctx`, and carries only `uid="<Cordis Fiber.uid>"`; Context elements have no attributes. Context-only `extend()`, `isolate()`, and `intercept()` layers remain direct Context descendants.
@@ -77,16 +104,21 @@ Host and Client publish the same nested `CordisTreeSnapshot` type. Context and F
 
 When a Client disconnects, its Console execution context and live object ids are destroyed immediately. With disconnected-tree retention enabled, Elements keeps the last tree unchanged while connection state remains in the inspection model rather than becoming an unreviewed DOM attribute. Reconnection keeps the logical source id, creates a new synthetic CDP context id for the new transport generation, and replaces the stale tree after its complete snapshot arrives. The Worker retains at most `maxDisconnectedCordisTrees` such snapshots; zero removes them immediately.
 
+<a id="host-fetch-capture"></a>
 ## Host fetch capture
 
 Fetch capture is on by default and records the complete URL, all request and response headers, request body, response body, status, timing, errors, and cancellation. It does not redact credentials, cookies, query values, or payloads. Body capture reads clones; the caller receives the original Response as soon as the original fetch resolves.
 
 The configured body limits bound retention rather than select fields: capture keeps the prefix and marks the result truncated. `Network.getRequestPostData` and `Network.getResponseBody` read the Worker's retained bytes. `Network.streamResourceContent` returns the buffered prefix and adds later response bytes to `Network.dataReceived` for that DevTools connection, which drives live Response and EventStream views. Direct Undici Client/Dispatcher calls and fetch references retained before plugin activation are outside this observer.
 
+After response headers arrive, a caller-side abort ends clone capture as a retained, possibly truncated response rather than a failed request. A fetch rejection before response headers remains a failed request.
+
+<a id="security"></a>
 ## Security
 
 The CDP target grants arbitrary code execution in both Host and connected Client realms through `Runtime.evaluate`; Host Debugger operations provide additional control. Full fetch capture includes secrets. The Worker therefore accepts only a `127.0.0.1` bind address. Client ingest additionally requires a random WebSocket subprotocol token injected by the Host and rejects non-loopback origins unless explicitly configured. The CDP socket itself has no token; loopback binding is its only access control.
 
+<a id="model-experience"></a>
 ## Model Experience
 
 None, as this developer-only inspector observes runtime activity without changing model requests.
@@ -97,9 +129,21 @@ None; this package neither assembles nor sends a provider request.
 
 ## Known Limitations and Deferred Work
 
+<a id="known-limitations-and-deferred-work"></a>
+
 - **Client active debugging is unsupported** — Console events, Runtime evaluation, RemoteObject access, and read-only `lib/client.js` Sources work. Client-script debugger requests return explicit unsupported errors; target-wide pause and resume control the Host only.
 - **Client Sources expose the Inspector bundle only** — other page scripts are not cataloged by this package.
 - **Client evaluation uses page JavaScript** — page Content Security Policy can block dynamic evaluation, and the synthetic context does not provide DevTools command-line helpers or native REPL declaration semantics.
 - **Fetch interception covers `globalThis.fetch`** — direct Undici APIs and fetch references retained before activation are not observed.
 - **Body cloning has cost** — full capture tees request and response streams up to the configured limits and can increase memory and I/O pressure. The retained-body limit does not include buffering inside the stream tee, including an oversized source chunk or data queued for a slower application reader.
 - **No automatic Worker restart** — an unexpected Worker exit fails the current Inspector instance; lifecycle recovery belongs to a later change.
+
+<a id="dev-note"></a>
+### Dev Note
+
+<details>
+<summary>Working context for maintainers — click to expand</summary>
+
+None.
+
+</details>

+ 45 - 1
packages/experimental/inspector/README.zh.md

@@ -1,11 +1,33 @@
+---
+description: "面向 Host 与浏览器 Client Cordis 运行时的实验性 Chrome DevTools 检查,包括 Console 求值、Sources、Network 采集、Elements 树和独立于 CDP 的查询 API。"
+kind: "package-reference"
+---
+
 # @deepseek-ai/dsh-experimental-inspector
 
 [English](README.md) | 中文
 
-实验性 Client/Host 双面 Cordis 插件,在 Node worker thread 中提供一个 Chrome DevTools Protocol target。Host 与浏览器 Client 向 Worker 发布带版本的观测记录;Worker 独占 CDP 状态与输出。
+## 概述
+
+使用这个实验性 Inspector,可以在 Chrome DevTools 中检查一个运行中的 dsh Host 及其浏览器 Client。它提供 Host 与 Client Console context、Host Sources 与调试、Host fetch 采集和共享 Cordis 树,并让 Worker 独占全部 CDP 状态。
 
 本包为私有包,不进入正式发布。Worker 不访问实时 Cordis 对象;共享 Host/Client collector 会在传输前把它们投影成已验证 snapshot。Cordis 还负责插件组合、注册 `ctx.inspector`、注入 bootstrap 和资源释放。
 
+## 目录
+
+- [运行时布局](#runtime-layout)
+- [配置](#configuration)
+- [观测 API](#observation-api)
+- [Cordis 树检查](#cordis-tree-inspection)
+- [Host fetch 采集](#host-fetch-capture)
+- [安全](#security)
+- [模型体验](#model-experience)
+- [已知限制与延期工作](#known-limitations-and-deferred-work)
+- [开发备注](#dev-note)
+
+-----
+
+<a id="runtime-layout"></a>
 ## 运行时布局
 
 Host 插件启动 Worker 并连接专用 `MessagePort`。Client 插件读取注入的 `globalThis.__DSH_INSPECTOR__` bootstrap,直接向 Worker 打开一条独立、带鉴权的 WebSocket。Chrome DevTools 连接 Worker 的 CDP WebSocket。每条 DevTools 连接在 Worker 中独占一个连接 Host 主线程的 `node:inspector.Session`,因此 Host JavaScript 暂停时,Host Console 求值、Sources、断点和 resume 仍然可用。
@@ -18,6 +40,7 @@ Client source 声明类型化 Runtime、Console 和只读 Sources 能力。`Runt
 
 两个插件面运行同一份浏览器安全 Cordis collector。它把可达 Context 与 Fiber 对象转换成有版本的 `CordisTreeSnapshot`;Worker 存储这份与 CDP 无关的表示,并把每个 Host 或 Client source 投影到 Elements 面板。
 
+<a id="configuration"></a>
 ## 配置
 
 Host 插件注入 `webServer`,接受以下字段:
@@ -49,8 +72,11 @@ Host 插件注入 `webServer`,接受以下字段:
 | `maxCordisNodes` | `2048` | 一个 realm snapshot 截断前允许的 Context 与 Fiber 节点数 |
 | `maxDisconnectedCordisTrees` | `8` | 作为非实时 snapshot 保留的最近断联 realm 树数量 |
 
+生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-inspector)是全部已接受字段及其声明的详尽来源。
+
 Worker 监听后,Host 会记录一个 `devtools://` URL。同一个 Worker 提供 `/json`、`/json/list`、`/json/version`、`/devtools/page/<id>` target WebSocket 和 `/ingest` Client source。
 
+<a id="observation-api"></a>
 ## 观测 API
 
 两个插件面都提供同一个服务:
@@ -69,6 +95,7 @@ await ctx.inspector.cordis.getTree()
 
 发布操作先验证无损 JSON,再调度发送,不等待 Worker。每个 source 的队列都有上限;溢出表现为 sequence gap,绝不延迟被观察的应用操作。`cordis.getTree()` 读取 Worker 最新的 detached semantic snapshot,不创建 CDP session,也不启用 Runtime、Debugger 或 Sources。
 
+<a id="cordis-tree-inspection"></a>
 ## Cordis tree inspection
 
 Elements document 包含固定的 `<host>` 与 `<clients>` 容器。`<host>` 包含 Host root Context;`<clients>` 为每个 Client source 包含一个 `<client>`,每个 `<client>` 再包含该 realm 的 root Context。Cordis root Fiber 不显示。其他 Fiber 都是 `fiber.parent` 的子节点,并包含唯一一个表示 `fiber.ctx` 的 Context 子节点;Fiber 只携带 `uid="<Cordis Fiber.uid>"`,Context element 不携带 attribute。只有 Context 的 `extend()`、`isolate()` 与 `intercept()` 层仍然是直接 Context 后代。
@@ -77,16 +104,21 @@ Host 与 Client 发布同一种嵌套 `CordisTreeSnapshot` 类型。Context 与
 
 Client 断联时,其 Console execution context 与 live object id 会立即销毁。启用断联树保留后,Elements 会原样保留最后一棵树;连接状态留在 inspection model 中,不会未经设计就成为 DOM attribute。重连会沿用逻辑 source id,为新的 transport generation 创建新的 synthetic CDP context id,并在完整 snapshot 到达后替换旧树。Worker 最多保留 `maxDisconnectedCordisTrees` 棵此类 snapshot;设为零会立即移除。
 
+<a id="host-fetch-capture"></a>
 ## Host fetch 采集
 
 fetch 采集默认开启,记录完整 URL、全部请求与响应 headers、请求体、响应体、状态、时间、错误和取消。它不脱敏 credential、Cookie、query value 或 payload。body 采集读取 clone;原始 fetch resolve 后,调用方立即拿到原始 Response。
 
 配置的 body 上限限制保留量,而不选择字段:采集保留前缀并标记 truncated。`Network.getRequestPostData` 与 `Network.getResponseBody` 读取 Worker 保留的字节。`Network.streamResourceContent` 返回已缓冲的前缀,并仅为发起调用的 DevTools 连接把后续 response 字节附加到 `Network.dataReceived`,以驱动实时 Response 与 EventStream 视图。直接调用 Undici Client/Dispatcher,以及插件激活前保存的 fetch 引用,不在观察范围内。
 
+response headers 到达后,调用方 abort 会结束 clone 采集,并保留一份可能 truncated 的 response,而不会把整个请求标记为失败。response headers 到达前发生的 fetch rejection 仍然是失败请求。
+
+<a id="security"></a>
 ## 安全
 
 CDP target 通过 `Runtime.evaluate` 提供 Host 和已连接 Client realm 中的任意代码执行能力,Host Debugger 操作还会提供额外控制,完整 fetch 采集也包含秘密。因此 Worker 只接受 `127.0.0.1` 监听地址。Client ingest 还要求 Host 注入的随机 WebSocket subprotocol token;除非配置明确允许,否则拒绝非 loopback origin。CDP socket 本身不携带 token,loopback 监听是它唯一的访问控制。
 
+<a id="model-experience"></a>
 ## 模型体验
 
 无:这个仅供开发者使用的 Inspector 只观察运行时活动,不改变模型请求。
@@ -97,9 +129,21 @@ CDP target 通过 `Runtime.evaluate` 提供 Host 和已连接 Client realm 中
 
 ## Known Limitations and Deferred Work
 
+<a id="known-limitations-and-deferred-work"></a>
+
 - **Client active debugging 不受支持**——Console event、Runtime 求值、RemoteObject 访问和只读 `lib/client.js` Sources 可用。Client script debugger request 返回明确的 unsupported error;target-wide pause 与 resume 只控制 Host。
 - **Client Sources 只暴露 Inspector bundle**——本包不收录页面中的其他 script。
 - **Client 求值使用页面 JavaScript**——页面 Content Security Policy 可能阻止动态求值;synthetic context 不提供 DevTools command-line helper 或原生 REPL 声明语义。
 - **fetch 拦截范围是 `globalThis.fetch`**——直接调用 Undici API,以及激活前保存的 fetch 引用不会被观察。
 - **body clone 有运行成本**——完整采集会 tee 请求与响应 stream,直至达到配置上限,可能增加内存与 I/O 压力。保留 body 的上限不包含 stream tee 内部的缓冲,包括来源提供的超大 chunk,或为读取较慢的应用分支排队的数据。
 - **不自动重启 Worker**——Worker 意外退出会使当前 Inspector 实例失败;生命周期恢复留待后续改动。
+
+<a id="dev-note"></a>
+### 开发备注
+
+<details>
+<summary>维护者的工作上下文——点击展开</summary>
+
+无。
+
+</details>

+ 4 - 45
packages/experimental/inspector/src/client/bridge/rpc.ts

@@ -1,62 +1,21 @@
 /** Client-side non-CDP query bridge over the active Worker WebSocket. */
 
 import type { InspectorSourceDescriptor } from '../../shared/bridge/messages/observation.ts'
-import type { InspectorQuery, InspectorQueryResultFor } from '../../shared/bridge/messages/query/commands.ts'
-import { InspectorQueryConnection, type InspectorQueryConnectionOptions } from '../../shared/bridge/rpc.ts'
+import { InspectorQueryConnection } from '../../shared/bridge/rpc.ts'
 
 /** Owns query correlation across reconnecting Client source generations. */
-export class ClientBridgeRpc {
-  private readonly connection: InspectorQueryConnection
-
-  constructor(options: InspectorQueryConnectionOptions) {
-    this.connection = new InspectorQueryConnection(options)
-  }
-
+export class ClientBridgeRpc extends InspectorQueryConnection {
   /**
    * Connect query writes to one accepted Client WebSocket generation.
    * @param source - Accepted source descriptor.
    * @param socket - Active source WebSocket.
    */
-  connect(source: InspectorSourceDescriptor, socket: WebSocket): void {
-    this.connection.connect(source.sourceId, source.generation, {
+  connectSocket(source: InspectorSourceDescriptor, socket: WebSocket): void {
+    this.connect(source.sourceId, source.generation, {
       send: (frame) => {
         if (socket.readyState !== WebSocket.OPEN) throw new Error('Inspector Client query socket is not connected')
         socket.send(JSON.stringify(frame))
       },
     })
   }
-
-  /**
-   * Consume a potential query response.
-   * @param value - Decoded Worker message.
-   * @returns Whether the message belonged to this RPC protocol.
-   */
-  receive(value: unknown): boolean {
-    return this.connection.receive(value)
-  }
-
-  /**
-   * Execute one non-CDP query through the active Client generation.
-   * @param query - Typed query operation.
-   * @returns Its correlated typed result.
-   */
-  request<Query extends InspectorQuery>(query: Query): Promise<InspectorQueryResultFor<Query>> {
-    return this.connection.request(query)
-  }
-
-  /**
-   * Reject pending requests while permitting a later Client generation.
-   * @param reason - Failure reported to pending callers.
-   */
-  disconnect(reason: string): void {
-    this.connection.disconnect(reason)
-  }
-
-  /**
-   * Permanently reject all current and future requests.
-   * @param reason - Failure reported to pending callers.
-   */
-  close(reason: string): void {
-    this.connection.close(reason)
-  }
 }

+ 7 - 24
packages/experimental/inspector/src/client/bridge/transport.ts

@@ -2,15 +2,14 @@
 
 import type { InspectorClientBootstrap } from '../../shared/bridge/messages/control.ts'
 import type { InspectorSourceGeneration } from '../../shared/bridge/ids.ts'
-import { isJsonValue, jsonByteLength, type InspectorJsonValue } from '../../shared/json.ts'
-import type { InspectorQuery, InspectorQueryResultFor } from '../../shared/bridge/messages/query/commands.ts'
+import { isJsonValue, jsonByteLength } from '../../shared/json.ts'
 import {
   INSPECTOR_PROTOCOL_VERSION,
   parseWorkerSourceFrame,
   type SourceCloseFrame,
   type SourceOpenFrame,
 } from '../../shared/bridge/messages/observation.ts'
-import type { InspectorConnection } from '../../shared/bridge/publisher.ts'
+import { InspectorSourceConnection } from '../../shared/bridge/publisher.ts'
 import { ClientConsoleObserver } from '../cdp/console.ts'
 import { ClientRuntimeExecutor } from '../cdp/runtime.ts'
 import {
@@ -27,16 +26,16 @@ import { ClientBridgeRpc } from './rpc.ts'
 import { dispatchBridgeFrame } from './dispatcher.ts'
 
 /** Reconnecting Client source whose bounded queue never blocks page work. */
-export class ClientInspectorSource implements InspectorConnection {
+export class ClientInspectorSource extends InspectorSourceConnection {
   private readonly realmSource: ClientRealmSource
-  private readonly publisher: ClientBridgePublisher
+  protected readonly publisher: ClientBridgePublisher
   private socket: WebSocket | undefined
   private generation: InspectorSourceGeneration | undefined
   private accepted = false
   private closed = false
   private readonly runtime: ClientRuntimeExecutor
   private readonly console: ClientConsoleObserver
-  private readonly queries: ClientBridgeRpc
+  protected readonly queries: ClientBridgeRpc
   private readonly lifecycle: ClientBridgeLifecycle
 
   constructor(
@@ -44,6 +43,7 @@ export class ClientInspectorSource implements InspectorConnection {
     label = document.title || 'Client',
     private readonly sourceCatalog: ClientSourceCatalog | undefined = discoverInspectorClientSourceCatalog(),
   ) {
+    super()
     this.realmSource = new ClientRealmSource(label)
     this.lifecycle = new ClientBridgeLifecycle(bootstrap.reconnectBaseMs, bootstrap.reconnectMaxMs)
     this.publisher = new ClientBridgePublisher({
@@ -87,23 +87,6 @@ export class ClientInspectorSource implements InspectorConnection {
     this.connect()
   }
 
-  /** Publish one JSON observation without waiting on the ingest socket. */
-  publish(topic: string, payload: InspectorJsonValue, monotonicMs = performance.now()): void {
-    if (this.closed) return
-    this.publisher.publish(topic, payload, monotonicMs)
-  }
-
-  /** Retain and publish one state value for reconnect and resnapshot recovery. */
-  setState(topic: string, payload: InspectorJsonValue, monotonicMs = performance.now()): void {
-    if (this.closed) throw new Error('inspector: Client source is closed')
-    this.publisher.setState(topic, payload, monotonicMs)
-  }
-
-  /** Execute one non-CDP query through the accepted Client source generation. */
-  request<Query extends InspectorQuery>(query: Query): Promise<InspectorQueryResultFor<Query>> {
-    return this.queries.request(query)
-  }
-
   /** Permanently stop reconnecting and close the active source generation. */
   close(): void {
     if (this.closed) return
@@ -167,7 +150,7 @@ export class ClientInspectorSource implements InspectorConnection {
           accepted: () => {
             this.accepted = true
             this.lifecycle.connected()
-            this.queries.connect(source, socket)
+            this.queries.connectSocket(source, socket)
             this.publisher.accept(socket)
           },
           resnapshot: () => { this.publisher.replace(socket) },

+ 2 - 24
packages/experimental/inspector/src/client/inspection/cordis.ts

@@ -1,25 +1,3 @@
-/** Browser adapter that publishes shared Cordis snapshots over the Client bridge. */
+/** Client entry for the shared Cordis snapshot publisher. */
 
-import type { Context } from '@deepseek-ai/cordis'
-import type { CordisTreeLimits } from '../../shared/cordis/collector.ts'
-import { observeCordisTree } from '../../shared/cordis/observer.ts'
-import { CORDIS_TREE_TOPIC } from '../../shared/bridge/messages/cordis.ts'
-import type { InspectorJsonValue } from '../../shared/json.ts'
-import type { InspectorStatePublisher } from '../../shared/bridge/publisher.ts'
-
-/**
- * Observe the Client Cordis runtime and retain its latest bridge snapshot.
- * @param ctx - Client plugin context whose root is inspected.
- * @param publisher - Active Client bridge publisher.
- * @param limits - Snapshot node and encoded-byte limits.
- * @returns A disposer that stops observation and releases retained objects.
- */
-export function publishCordisTree(
-  ctx: Context,
-  publisher: InspectorStatePublisher,
-  limits: CordisTreeLimits,
-): () => void {
-  return observeCordisTree(ctx, (snapshot) => {
-    publisher.setState(CORDIS_TREE_TOPIC, snapshot as unknown as InspectorJsonValue)
-  }, limits)
-}
+export { publishCordisTree } from '../../shared/cordis/publisher.ts'

+ 4 - 41
packages/experimental/inspector/src/host/bridge/rpc.ts

@@ -2,58 +2,21 @@
 
 import type { MessagePort } from 'node:worker_threads'
 import type { InspectorSourceDescriptor } from '../../shared/bridge/messages/observation.ts'
-import type { InspectorQuery, InspectorQueryResultFor } from '../../shared/bridge/messages/query/commands.ts'
 import { InspectorQueryConnection, type InspectorQueryConnectionOptions } from '../../shared/bridge/rpc.ts'
 
 /** Owns query correlation for one Host source generation. */
-export class HostBridgeRpc {
-  private readonly connection: InspectorQueryConnection
-
+export class HostBridgeRpc extends InspectorQueryConnection {
   constructor(private readonly port: MessagePort, options: InspectorQueryConnectionOptions) {
-    this.connection = new InspectorQueryConnection(options)
+    super(options)
   }
 
   /**
    * Connect query writes after the Worker accepts the Host source.
    * @param source - Accepted Host source descriptor.
    */
-  connect(source: InspectorSourceDescriptor): void {
-    this.connection.connect(source.sourceId, source.generation, {
+  connectPort(source: InspectorSourceDescriptor): void {
+    this.connect(source.sourceId, source.generation, {
       send: (frame) => { this.port.postMessage(frame) },
     })
   }
-
-  /**
-   * Consume a potential query response.
-   * @param value - Decoded Worker message.
-   * @returns Whether the message belonged to this RPC protocol.
-   */
-  receive(value: unknown): boolean {
-    return this.connection.receive(value)
-  }
-
-  /**
-   * Execute one non-CDP query through the active Host generation.
-   * @param query - Typed query operation.
-   * @returns Its correlated typed result.
-   */
-  request<Query extends InspectorQuery>(query: Query): Promise<InspectorQueryResultFor<Query>> {
-    return this.connection.request(query)
-  }
-
-  /**
-   * Reject pending requests while retaining the reusable Host bridge.
-   * @param reason - Failure reported to pending callers.
-   */
-  disconnect(reason: string): void {
-    this.connection.disconnect(reason)
-  }
-
-  /**
-   * Permanently reject all current and future requests.
-   * @param reason - Failure reported to pending callers.
-   */
-  close(reason: string): void {
-    this.connection.close(reason)
-  }
 }

+ 6 - 24
packages/experimental/inspector/src/host/bridge/transport.ts

@@ -1,7 +1,6 @@
 /** Host-realm observation publisher over a dedicated MessagePort. */
 
 import type { MessagePort } from 'node:worker_threads'
-import type { InspectorQuery, InspectorQueryResultFor } from '../../shared/bridge/messages/query/commands.ts'
 import {
   INSPECTOR_PROTOCOL_VERSION,
   parseWorkerSourceFrame,
@@ -9,11 +8,10 @@ import {
   type SourceOpenFrame,
   type WorkerToSourceFrame,
 } from '../../shared/bridge/messages/observation.ts'
-import type { InspectorConnection } from '../../shared/bridge/publisher.ts'
+import { InspectorSourceConnection } from '../../shared/bridge/publisher.ts'
 import { createHostRealmSource } from '../inspection/realm.ts'
 import { HostBridgePublisher } from './publisher.ts'
 import { HostBridgeRpc } from './rpc.ts'
-import type { InspectorJsonValue } from '../../shared/json.ts'
 import { dispatchBridgeFrame } from './dispatcher.ts'
 
 /** Buffer limits for one source publisher. */
@@ -28,13 +26,14 @@ export interface HostSourceOptions {
 }
 
 /** Non-blocking Host source; queue overflow is represented by `droppedBefore` on the next batch. */
-export class HostInspectorSource implements InspectorConnection {
+export class HostInspectorSource extends InspectorSourceConnection {
   private readonly source
-  private readonly publisher: HostBridgePublisher
+  protected readonly publisher: HostBridgePublisher
   private closed = false
-  private readonly queries: HostBridgeRpc
+  protected readonly queries: HostBridgeRpc
 
   constructor(private readonly port: MessagePort, options: HostSourceOptions) {
+    super()
     this.source = createHostRealmSource(options.label)
     this.publisher = new HostBridgePublisher(port, this.source, options)
     this.queries = new HostBridgeRpc(port, {
@@ -61,23 +60,6 @@ export class HostInspectorSource implements InspectorConnection {
     this.publisher.replace()
   }
 
-  /** Publish one observation without waiting on Worker processing. */
-  publish(topic: string, payload: InspectorJsonValue, monotonicMs = performance.now()): void {
-    if (this.closed) return
-    this.publisher.publish(topic, payload, monotonicMs)
-  }
-
-  /** Retain and publish one state value for future `source/replace` frames. */
-  setState(topic: string, payload: InspectorJsonValue, monotonicMs = performance.now()): void {
-    if (this.closed) throw new Error('inspector: Host source is closed')
-    this.publisher.setState(topic, payload, monotonicMs)
-  }
-
-  /** Execute one non-CDP query through the accepted Host source generation. */
-  request<Query extends InspectorQuery>(query: Query): Promise<InspectorQueryResultFor<Query>> {
-    return this.queries.request(query)
-  }
-
   /** Flush pending observations and close the source port. */
   close(): void {
     if (this.closed) return
@@ -98,7 +80,7 @@ export class HostInspectorSource implements InspectorConnection {
     if (frame.t !== 'source/rejected'
       && (frame.sourceId !== this.source.sourceId || frame.generation !== this.source.generation)) return
     dispatchBridgeFrame(frame, {
-      accepted: () => { this.queries.connect(this.source) },
+      accepted: () => { this.queries.connectPort(this.source) },
       resnapshot: () => { this.publisher.replace() },
       rejected: (rejected) => { this.queries.disconnect(`Inspector Host source rejected: ${rejected.message}`) },
     })

+ 13 - 11
packages/experimental/inspector/src/host/cdp/index.ts

@@ -8,19 +8,21 @@ import { profilerBridgeCapability } from './profiler.ts'
 import { runtimeBridgeCapability } from './runtime.ts'
 import { sourcesBridgeCapability } from './sources.ts'
 
+const HOST_BRIDGE_CAPABILITIES: readonly InspectorSourceCapability[] = [
+  runtimeBridgeCapability(''),
+  consoleBridgeCapability(),
+  sourcesBridgeCapability(false),
+  debuggerBridgeCapability(),
+  profilerBridgeCapability(),
+  heapProfilerBridgeCapability(),
+].filter((capability): capability is InspectorSourceCapability => capability !== undefined)
+
 /**
  * Collect Host source-bridge capabilities.
- * @param origin - Unused Host origin supplied for parity with the Client adapter.
- * @param hasSources - Unused source availability supplied for parity with the Client adapter.
+ * @param _origin - Unused Host origin supplied for parity with the Client adapter.
+ * @param _hasSources - Unused source availability supplied for parity with the Client adapter.
  * @returns No capabilities because the Worker attaches to Host V8 directly.
  */
-export function bridgeCapabilities(origin: string, hasSources: boolean): readonly InspectorSourceCapability[] {
-  return [
-    runtimeBridgeCapability(origin),
-    consoleBridgeCapability(),
-    sourcesBridgeCapability(hasSources),
-    debuggerBridgeCapability(),
-    profilerBridgeCapability(),
-    heapProfilerBridgeCapability(),
-  ].filter((capability): capability is InspectorSourceCapability => capability !== undefined)
+export function bridgeCapabilities(_origin: string, _hasSources: boolean): readonly InspectorSourceCapability[] {
+  return HOST_BRIDGE_CAPABILITIES
 }

+ 2 - 24
packages/experimental/inspector/src/host/inspection/cordis.ts

@@ -1,25 +1,3 @@
-/** Host adapter that publishes shared Cordis snapshots over the Host bridge. */
+/** Host entry for the shared Cordis snapshot publisher. */
 
-import type { Context } from '@deepseek-ai/cordis'
-import type { CordisTreeLimits } from '../../shared/cordis/collector.ts'
-import { observeCordisTree } from '../../shared/cordis/observer.ts'
-import { CORDIS_TREE_TOPIC } from '../../shared/bridge/messages/cordis.ts'
-import type { InspectorJsonValue } from '../../shared/json.ts'
-import type { InspectorStatePublisher } from '../../shared/bridge/publisher.ts'
-
-/**
- * Observe the Host Cordis runtime and retain its latest bridge snapshot.
- * @param ctx - Host plugin context whose root is inspected.
- * @param publisher - Active Host bridge publisher.
- * @param limits - Snapshot node and encoded-byte limits.
- * @returns A disposer that stops observation and releases retained objects.
- */
-export function publishCordisTree(
-  ctx: Context,
-  publisher: InspectorStatePublisher,
-  limits: CordisTreeLimits,
-): () => void {
-  return observeCordisTree(ctx, (snapshot) => {
-    publisher.setState(CORDIS_TREE_TOPIC, snapshot as unknown as InspectorJsonValue)
-  }, limits)
-}
+export { publishCordisTree } from '../../shared/cordis/publisher.ts'

+ 22 - 1
packages/experimental/inspector/src/shared/bridge/publisher.ts

@@ -1,7 +1,7 @@
 /** Source-side interfaces shared by MessagePort and WebSocket bridge implementations. */
 
 import type { InspectorJsonValue } from '../json.ts'
-import type { InspectorQueryRequester } from './messages/query/commands.ts'
+import type { InspectorQuery, InspectorQueryRequester, InspectorQueryResultFor } from './messages/query/commands.ts'
 
 /** Transport-independent observation publisher. */
 export interface InspectorPublisher {
@@ -22,3 +22,24 @@ export interface InspectorStatePublisher extends InspectorPublisher {
 
 /** Shared capabilities exposed above a Host MessagePort or Client WebSocket carrier. */
 export interface InspectorConnection extends InspectorStatePublisher, InspectorQueryRequester {}
+
+/** Shared observation and query delegation inherited by both source transports. */
+export abstract class InspectorSourceConnection implements InspectorConnection {
+  protected abstract readonly publisher: InspectorStatePublisher
+  protected abstract readonly queries: InspectorQueryRequester
+
+  /** Publish one JSON observation without waiting on its carrier. */
+  publish(topic: string, payload: InspectorJsonValue, monotonicMs = performance.now()): void {
+    this.publisher.publish(topic, payload, monotonicMs)
+  }
+
+  /** Retain and publish one state value for reconnect or replacement recovery. */
+  setState(topic: string, payload: InspectorJsonValue, monotonicMs = performance.now()): void {
+    this.publisher.setState(topic, payload, monotonicMs)
+  }
+
+  /** Execute one non-CDP query through the active source generation. */
+  request<Query extends InspectorQuery>(query: Query): Promise<InspectorQueryResultFor<Query>> {
+    return this.queries.request(query)
+  }
+}

+ 25 - 0
packages/experimental/inspector/src/shared/cordis/publisher.ts

@@ -0,0 +1,25 @@
+/** Shared Host/Client publication of browser-safe Cordis snapshots. */
+
+import type { Context } from '@deepseek-ai/cordis'
+import { CORDIS_TREE_TOPIC } from '../bridge/messages/cordis.ts'
+import type { InspectorStatePublisher } from '../bridge/publisher.ts'
+import type { InspectorJsonValue } from '../json.ts'
+import type { CordisTreeLimits } from './collector.ts'
+import { observeCordisTree } from './observer.ts'
+
+/**
+ * Observe one Cordis runtime and retain its latest source snapshot.
+ * @param ctx - Plugin context whose root is inspected.
+ * @param publisher - Active Host or Client source publisher.
+ * @param limits - Snapshot node and encoded-byte limits.
+ * @returns A disposer that stops observation and releases retained objects.
+ */
+export function publishCordisTree(
+  ctx: Context,
+  publisher: InspectorStatePublisher,
+  limits: CordisTreeLimits,
+): () => void {
+  return observeCordisTree(ctx, (snapshot) => {
+    publisher.setState(CORDIS_TREE_TOPIC, snapshot as unknown as InspectorJsonValue)
+  }, limits)
+}

+ 3 - 1
packages/experimental/inspector/tests/client-browser.e2e.ts

@@ -168,7 +168,9 @@ describe.skipIf(!built)('Inspector built Client in Chromium', () => {
     })
     expect(evaluated.error).toBeUndefined()
     expect(asRecord(evaluated.result?.result).objectId).toMatch(/^runtime:/u)
-    expect(await page.evaluate(() => Reflect.get(globalThis, '__inspectorConsoleEvaluation'))).toEqual({ answer: 42 })
+    expect(await page.evaluate(() => Reflect.get(globalThis, '__inspectorConsoleEvaluation') as unknown)).toEqual({
+      answer: 42,
+    })
 
     await page.evaluate(() => {
       const value = { browser: true, nested: { ready: true } }

+ 1 - 1
packages/experimental/inspector/tests/fixtures/client-source.host.ts

@@ -6,7 +6,7 @@ import type { CordisRuntimeTree } from '../../src/shared/cordis/model.ts'
 import type { InspectorJsonValue } from '../../src/shared/json.ts'
 
 /** Optional source artifact exposed by the Client fixture. */
-export interface ClientFixtureSourceCatalog {
+interface ClientFixtureSourceCatalog {
   readonly sourceText: string
   readonly sourceMap: string
   readonly sourceUrl: string

+ 2 - 7
packages/experimental/inspector/tests/integration.host.spec.ts

@@ -90,13 +90,8 @@ describe('experimental Inspector real Worker', () => {
     await vi.waitFor(async () => {
       const response = await cdp!.call('DSHInspector.getSources')
       const sources = response.result?.sources as Array<{ kind: string; topics: Record<string, number> }>
-      expect(sources).toEqual(expect.arrayContaining([
-        expect.objectContaining({ kind: 'host', topics: { 'host/probe': 1 } }),
-        expect.objectContaining({
-          kind: 'client',
-          topics: expect.objectContaining({ 'client/probe': 1 }),
-        }),
-      ]))
+      expect(sources.find(source => source.kind === 'host')?.topics).toEqual({ 'host/probe': 1 })
+      expect(sources.find(source => source.kind === 'client')?.topics).toMatchObject({ 'client/probe': 1 })
     })
 
     ;(globalThis as Record<string, unknown>).__inspectorHostProbe = 73

+ 1 - 0
packages/experimental/inspector/tsconfig.client.json

@@ -72,6 +72,7 @@
     "src/shared/cordis/object-reference.ts",
     "src/shared/cordis/object-registry.ts",
     "src/shared/cordis/observer.ts",
+    "src/shared/cordis/publisher.ts",
     "src/shared/cordis/projector.ts",
     "src/shared/cordis/reader.ts",
     "src/shared/cordis/snapshot.ts",

+ 1 - 0
packages/experimental/inspector/tsconfig.host.json

@@ -74,6 +74,7 @@
     "src/shared/cordis/object-reference.ts",
     "src/shared/cordis/object-registry.ts",
     "src/shared/cordis/observer.ts",
+    "src/shared/cordis/publisher.ts",
     "src/shared/cordis/projector.ts",
     "src/shared/cordis/reader.ts",
     "src/shared/cordis/snapshot.ts",