chore(web): gate repairs for the config-tree boot round
Lint (bridge JSDoc params, service-class export shape, async invariant listener form), regenerated doc catalogs/graphs with role classifications for httpServer and clientModuleHost, catalog type-link exemptions for the route/graph contracts, knip alignment (apps/cli composes via cordis.yml so its yml-named deps are runtime edges knip cannot see; webserver's deleted test dir), the zh side of the loading-model note brought along with its pairing records, and coverage exclusions for the new web-transport halves under the GUI test-lane TODO (real-composition harnesses land with that lane).
This commit is contained in:
@@ -5,7 +5,13 @@
|
||||
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
|
||||
/** Bridge one node:http request to the fetch-shaped handler (client close aborts; SSE bodies stream out chunk by chunk). */
|
||||
/**
|
||||
* Bridge one node:http request to the fetch-shaped handler (client close
|
||||
* aborts; SSE bodies stream out chunk by chunk).
|
||||
* @param req - incoming node:http request (fully read before dispatch).
|
||||
* @param res - node:http response the bridge writes and owns to completion.
|
||||
* @param apiHandler - fetch-shaped API carrier the request is dispatched to.
|
||||
*/
|
||||
export async function bridge(req: IncomingMessage, res: ServerResponse, apiHandler: { fetch: typeof fetch }): Promise<void> {
|
||||
const abort = new AbortController()
|
||||
// Client-disconnect detection MUST hang off the response, not the request:
|
||||
|
||||
@@ -30,7 +30,10 @@ function statWatchers(): number {
|
||||
*/
|
||||
const install: InvariantInstaller = (ctx, fail) => {
|
||||
const baselines = new WeakMap<Fiber, number>()
|
||||
ctx.on('internal/plugin', (fiber) => {
|
||||
// Async listener by design: emitPluginDisposed awaits-and-logs returned
|
||||
// promises, so a violation surfaces loudly instead of unhandled.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
ctx.on('internal/plugin', async (fiber) => {
|
||||
if (fiber.name !== 'client-hmr') return
|
||||
if (fiber.uid !== null) {
|
||||
baselines.set(fiber, statWatchers())
|
||||
@@ -38,16 +41,12 @@ const install: InvariantInstaller = (ctx, fail) => {
|
||||
}
|
||||
const baseline = baselines.get(fiber)
|
||||
if (baseline === undefined) return
|
||||
// Returned to the emitter: emitPluginDisposed awaits-and-logs async
|
||||
// listener failures, so a violation surfaces loudly instead of unhandled.
|
||||
return (async () => {
|
||||
await Promise.resolve()
|
||||
await fiber.await()
|
||||
const remaining = statWatchers()
|
||||
if (remaining > baseline) {
|
||||
fail(`client-hmr fiber disposed but ${remaining - baseline} bundle stat watcher(s) survived teardown`)
|
||||
}
|
||||
})()
|
||||
await Promise.resolve()
|
||||
await fiber.await()
|
||||
const remaining = statWatchers()
|
||||
if (remaining > baseline) {
|
||||
fail(`client-hmr fiber disposed but ${remaining - baseline} bundle stat watcher(s) survived teardown`)
|
||||
}
|
||||
}, { global: true })
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/client.js",
|
||||
"lib/invariant.js",
|
||||
"lib/client.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
|
||||
@@ -192,7 +192,7 @@ export class ClientModuleHostService extends Service {
|
||||
for (const entry of ctx.loader.entries()) this.dirty.add(entry.options.name)
|
||||
this.composed = this.compose()
|
||||
const failures: Error[] = []
|
||||
this.flush((err) => failures.push(err))
|
||||
this.flush(err => failures.push(err))
|
||||
if (failures.length > 0) {
|
||||
throw new AggregateError(
|
||||
failures,
|
||||
|
||||
@@ -188,6 +188,32 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'clientModuleHost',
|
||||
summary: 'The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'graph(): WebBootGraph',
|
||||
jsDoc: '/**\n * Current composed entry graph (stable object between changes).\n * @returns the graph served as `window.__DSH_BOOT__`.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'clientPath(id: string): string | undefined',
|
||||
jsDoc: '/**\n * Absolute path of an entry\'s client bundle.\n * @param id - entry id (package name).\n * @returns the path, or undefined for an unknown id.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'rebuilt(id: string): string | undefined',
|
||||
jsDoc: '/**\n * Re-hash one bundle (the HMR watch\'s registration hook — the only entry\n * point through which bundle content changes reach the graph).\n * @param id - entry id (package name).\n * @returns the new rev, or undefined for an unknown id.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'onRebuilt(listener: (id: string, rev: string) => void): () => void',
|
||||
jsDoc: '/**\n * Subscribe to bundle rebuilds; fires only when the re-hash changed the rev.\n * @param listener - receives the entry id and its new bundle rev.\n * @returns the unsubscriber.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'onGraphChanged(listener: () => void): () => void',
|
||||
jsDoc: '/**\n * Fires after any flush that recomposed the graph (row added/removed, or a\n * rebuilt rev change). Pull model: listeners re-read {@link graph}.\n * @param listener - notified with no payload.\n * @returns the unsubscriber.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'codeRuntime',
|
||||
summary: 'Registers one `ctx.codeRuntime` implementation.',
|
||||
@@ -314,6 +340,20 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'httpServer',
|
||||
summary: 'The web-shape HTTP carrier service.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'register(route: WebRoute): () => void',
|
||||
jsDoc: '/**\n * Register a named route. Duplicate (kind, path) throws — route patterns are\n * a composition-level contract, so a collision is a misconfiguration.\n * @param route - kind, path, and the owning handler.\n * @returns the disposer removing the route.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'tapIndex(transform: (html: string) => string): () => void',
|
||||
jsDoc: '/**\n * Register an index.html transform, applied to every index response in\n * registration order.\n * @param transform - pure html-to-html function.\n * @returns the disposer removing the transform.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'invariants',
|
||||
summary: 'Package-owned invariant registry with global and regex-based selection.',
|
||||
@@ -2291,6 +2331,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'WebFetchResult',
|
||||
declaration: 'export interface WebFetchResult {\n readonly url: string;\n readonly statusCode: number;\n readonly body: WebFetchBody;\n readonly truncated: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WebRoute',
|
||||
declaration: 'export interface WebRoute {\n kind: WebRouteKind;\n path: string;\n handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WebRouteKind',
|
||||
declaration: 'export type WebRouteKind = \'exact\' | \'prefix\';',
|
||||
},
|
||||
{
|
||||
name: 'WebSearchProvider',
|
||||
declaration: 'export interface WebSearchProvider {\n readonly id: string;\n available(): boolean;\n search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;\n}',
|
||||
|
||||
@@ -61,7 +61,9 @@ export class ApiProxyService extends Service implements ApiProxy {
|
||||
this.sessions = api.sessions
|
||||
this.host = api.host
|
||||
this.events = api.events
|
||||
this.respond = api.respond
|
||||
// createApiProxy returns closures (no `this` capture); bind only satisfies
|
||||
// the unbound-method lint without changing behavior.
|
||||
this.respond = api.respond.bind(api)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user