Files
deepseek-harness/packages/host/webserver/src/plugin-events.ts
imccyu fb47f61a83 refactor(gui): host graph from dshClient discovery; webserver self-watches bundles for HMR
The registry scans mounted Loader entries' dshClient declarations and
composes __DSH_BOOT__ {rev, entries} — inject edges and the immediately
mark come from manifests, never hand-copied; malformed fields fail loud
at load. The composing app owns one flat roster plus the --dev switch
(hmr row and bundle watching are dev-graph decisions).

The rebuild signal is the webserver's own observation: in dev mode the
registry stat-polls each scanned bundle (fs.watchFile; polling because
network mounts deliver no inotify), re-hashes on change, and broadcasts
a rebuilt frame on the /plugins/events SSE channel only when the rev
actually changed. Watch membership follows the table across rescans;
dispose drops all watches; a torn read self-heals on the next tick.
The POST /plugins/rebuilt endpoint is gone — builders and the host
share zero protocol. dsh web --dev logs the watched bundle list and
each rebuilt id with its rev transition.
2026-07-24 02:09:42 +08:00

57 lines
2.1 KiB
TypeScript

/**
* `/plugins/events` SSE channel: the system-side push surface for the client
* entry graph (connect → current graph frame; dev rebuild → rebuilt frame).
* Presentation-only wire — frames never enter the session log (distinct from
* the /api/* session SSE, which is api-contract territory). Connections are
* plain node:http responses held in a set; the server's closeAllConnections
* tears them down on shutdown.
*/
import type { ServerResponse } from 'node:http'
import type { WebBootGraph } from './web-plugins.ts'
/** One `/plugins/events` frame: the full graph on connect, or one rebuilt bundle notice. */
export type PluginEventFrame =
| { type: 'graph'; graph: WebBootGraph }
| { type: 'rebuilt'; id: string; rev: string }
/** Broadcast surface owned by the webserver routing layer. */
export interface PluginEventChannel {
/** Adopt one incoming SSE request: writes the SSE preamble and the current-graph frame, then keeps the response open. */
connect(res: ServerResponse, graph: WebBootGraph): void
/** Push one frame to every open connection. */
broadcast(frame: PluginEventFrame): void
}
/** Serialize one frame as an SSE data line. */
function sseData(frame: PluginEventFrame): string {
return `data: ${JSON.stringify(frame)}\n\n`
}
/**
* Create the channel (one per running server).
* @returns the connect/broadcast surface.
*/
export function createPluginEventChannel(): PluginEventChannel {
const connections = new Set<ServerResponse>()
return {
connect(res, graph) {
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
'connection': 'keep-alive',
})
// Comment line on open so clients/proxies see a live channel even when
// no rebuild ever happens; EventSource frame parsing skips it naturally.
res.write(': connected\n\n')
res.write(sseData({ type: 'graph', graph }))
connections.add(res)
res.on('close', () => { connections.delete(res) })
},
broadcast(frame) {
const line = sseData(frame)
for (const res of connections) res.write(line)
},
}
}