mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
permission, plan, invariants, http-server, storage (hub + backend seam + domain form + domain/changed), workspace, tui, and client-modules complete the docs/subsystems tier: every ctx service and event scope now has one owning page, the precondition for generating per-subsystem service/event reference into these pages. 25 new type-equiv manifest entries; 16 types move from TYPE_LINK_EXEMPTIONS to LINK_MAP now that they have catalog homes (dead InvariantRegistration exemption removed; catalogs regenerated); core.md's sub-page table gains the eight rows in both languages; the owning subsystems-catalog Agent Note records the coverage extension. Chinese counterparts and pair records follow in the next commit.
64 lines
6.3 KiB
Markdown
64 lines
6.3 KiB
Markdown
# Client Modules
|
|
|
|
English | [中文](client-modules.zh.md)
|
|
|
|
The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModuleHost` (`ClientModuleHostService`). It scans the host Loader's entries for `dshClient` packages, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins/<id>/client.js`, and taps the index render to inject the boot manifest — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [http-server.md](http-server.md) supplies the prefix route and index tap this service registers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here.
|
|
|
|
Source: [`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
|
|
|
|
## The wire
|
|
|
|
The graph is the wire single source between the Node and browser halves: the host composes `WebBootEntry` rows from scanned packages, injects the graph as the first script in `<head>` (`window.__DSH_BOOT__`, with `<` escaped so plugin-controlled strings cannot break out of the script element), and the shell parses it before booting anything. A page without a valid manifest cannot boot — the browser-side parser throws loud on a missing or malformed graph.
|
|
|
|
```ts type-equiv
|
|
/**
|
|
* One composed client entry pushed by the host (web2 §0 graph row). Wire
|
|
* single source: the host node half (package root) produces this same shape.
|
|
* `immediately` marks stage-one prefetch; `inject` is informational graph
|
|
* metadata (the authoritative edges live in each package's dshClient
|
|
* declaration and reach fibers through entry creation).
|
|
*/
|
|
interface WebBootEntry {
|
|
/** Entry name == package name. */
|
|
id: string
|
|
/** Bundle endpoint, '/plugins/<id>/client.js?rev=<rev>'. */
|
|
url: string
|
|
/** Bundle content hash (cache-busting consistency anchor). */
|
|
rev: string
|
|
/** Package-name dependency edges, informational (preflight display / HMR diffing). */
|
|
inject?: string[]
|
|
/** Stage-one prefetch mark: fetch + execute (factory registration) during module-face boot. */
|
|
immediately?: boolean
|
|
}
|
|
```
|
|
|
|
```ts type-equiv
|
|
/** The composed client entry graph the host injects as `window.__DSH_BOOT__`. */
|
|
interface WebBootGraph {
|
|
/** Consistency anchor over the whole graph (content + bundle hashes). */
|
|
rev: string
|
|
/** Composed entries; order carries no semantics (activation order is fiber inject waiting). */
|
|
entries: WebBootEntry[]
|
|
}
|
|
```
|
|
|
|
Each row's `rev` is the bundle's content hash and rides the URL as a cache-busting query; the graph `rev` hashes the composed rows, so any row change changes it. `immediately` marks the stage-one prefetch tier (fetch and execute during module-face boot, registration only); a lazy row is fetched on first import.
|
|
|
|
## The scan
|
|
|
|
A package joins the table by declaring `dshClient` (`platform: 'web'`, optional `inject` edges, optional `immediately`) in its package.json and exporting its built bundle at `exports["./client"]`. Package resolution anchors at the config tree's `ctx.baseUrl` — the cordis.yml directory, whose package declares every composed plugin as a dependency — and construction throws when that anchor is unset.
|
|
|
|
Scanning is incremental per package; there is no full-rescan code path. Every cordis `internal/plugin` emission (fiber construction or disposal) marks the fiber's entry name dirty, and a microtask flush reconciles each dirty name against the live loader entries. The activation pass seeds the same dirty set with all current entries and flushes synchronously, so first scan and steady state share one implementation — with opposite failure postures. At activation, a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud `AggregateError` listing every broken package: the fiber FAILS and the boot's fail-loud sweep reports it. In steady state, a broken package logs a warning and must not poison the others.
|
|
|
|
Package metadata — including the negative "not a client package" verdict — is cached per name and never expires: plugin-set changes take effect on restart. A fiber restart reuses its row and rev untouched; bundle content changes reach the graph only through `rebuilt()`.
|
|
|
|
## The bundle route and index tap
|
|
|
|
`GET`/`HEAD /plugins/<id>/client.js` serves the registered bundle from disk with `no-cache` (the rev query, not HTTP caching, anchors consistency); other methods are 405. An unknown id — or a registered row whose bundle is unreadable because it has not been built yet — answers a loud 404 rather than letting the carrier's SPA fallback ship HTML as JavaScript. The index tap injects the current graph on every index render, so a reload always boots against the live composition.
|
|
|
|
## The service
|
|
|
|
`ClientModuleHostService` (`ctx.clientModuleHost`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxclientmodulehost--clientmodulehostservice). `graph()` returns the current composed graph (a stable object between changes) and `clientPath(id)` the bundle's absolute path. `rebuilt(id)` is the only entry point through which bundle content reaches the graph: it re-hashes the file, and only a real rev change recomposes the graph and notifies. `onRebuilt` fires per changed bundle with the new rev; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt rev change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush.
|
|
|
|
In development, [dsh-client-hmr](../../packages/client/hmr/README.md) is the registry's watch driver: its node half stat-polls every graph row's bundle from a synchronously captured baseline, calls `rebuilt(id)` on change, resyncs its watch set through `onGraphChanged`, and broadcasts rev changes to the browser half over SSE. Production graphs omit the HMR row entirely; the module host itself never watches files.
|