mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
/**
|
|
* node:http ↔ WHATWG fetch bridge for the /api transport (host side of the
|
|
* web carrier; the fetch-shaped handler itself is transport-agnostic).
|
|
*/
|
|
|
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
|
/** Transport-independent request handler consumed by the Host HTTP bridge. */
|
|
export interface FetchHandler {
|
|
/**
|
|
* Handle one standard Fetch request.
|
|
* @param request - request produced by the active transport bridge.
|
|
* @returns complete or streaming Fetch response.
|
|
*/
|
|
fetch(request: Request): Promise<Response>
|
|
}
|
|
|
|
/**
|
|
* 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: FetchHandler): Promise<void> {
|
|
const abort = new AbortController()
|
|
// Client-disconnect detection MUST hang off the response, not the request:
|
|
// since Node 16, IncomingMessage 'close' fires as soon as the request body is
|
|
// fully consumed (immediately for a bodyless GET), which would abort every SSE
|
|
// stream right after open. ServerResponse 'close' fires on connection teardown;
|
|
// writableEnded distinguishes a normal end() from the client going away.
|
|
res.on('close', () => {
|
|
if (!res.writableEnded) abort.abort()
|
|
})
|
|
const chunks: Buffer[] = []
|
|
for await (const chunk of req) chunks.push(chunk as Buffer)
|
|
/* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server
|
|
requests; the fields are only optional on the client-side IncomingMessage type */
|
|
const request = new Request(new URL(req.url ?? '/', 'http://dsh.internal'), {
|
|
method: req.method ?? 'GET',
|
|
headers: Object.fromEntries(Object.entries(req.headers).filter(([, v]) => typeof v === 'string') as [string, string][]),
|
|
...chunks.length > 0 ? { body: Buffer.concat(chunks) } : {},
|
|
signal: abort.signal,
|
|
})
|
|
const response = await apiHandler.fetch(request)
|
|
res.writeHead(response.status, Object.fromEntries(response.headers.entries()))
|
|
if (response.body === null) {
|
|
res.end()
|
|
return
|
|
}
|
|
for await (const chunk of response.body) {
|
|
// Backpressure: a false return means the socket buffer is full — wait for drain
|
|
// instead of buffering unboundedly (slow/suspended SSE consumers). 'close' also
|
|
// resolves so a mid-wait disconnect can't park this loop forever; the close
|
|
// handler above aborts the handler stream, which then ends the iteration.
|
|
if (!res.write(chunk)) {
|
|
await new Promise<void>((resolve) => {
|
|
const done = (): void => {
|
|
res.off('drain', done)
|
|
res.off('close', done)
|
|
resolve()
|
|
}
|
|
res.once('drain', done)
|
|
res.once('close', done)
|
|
})
|
|
}
|
|
}
|
|
res.end()
|
|
}
|