mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
108 lines
2.9 KiB
TypeScript
108 lines
2.9 KiB
TypeScript
import { Duplex } from 'node:stream'
|
|
import {
|
|
BROWSER_NETWORK_TUNNEL_CONNECT_TIMEOUT_MS,
|
|
type BrowserNetworkTunnelSocket
|
|
} from './browser-network-tunnel-stream-state'
|
|
|
|
/**
|
|
* Adapts an execution route's socket into the `Duplex` the local SOCKS server
|
|
* pipes to. Resolves only once the underlying transport reports `connect`, so
|
|
* the SOCKS success reply is never sent for a dial that already failed.
|
|
*/
|
|
export function openExecutionRouteSocketAsDuplex(
|
|
socket: BrowserNetworkTunnelSocket,
|
|
options: { connectTimeoutMs?: number } = {}
|
|
): Promise<Duplex> {
|
|
return new Promise<Duplex>((resolve, reject) => {
|
|
let settled = false
|
|
// Why (review P2-1): some transports flow buffered bytes on the nextTick
|
|
// queue while emitting 'connect' from a microtask — a server banner or TLS
|
|
// record can arrive before any consumer listener exists. Buffer from the
|
|
// very start and replay through the duplex.
|
|
const earlyData: Buffer[] = []
|
|
let deliver = (bytes: Buffer): void => {
|
|
earlyData.push(bytes)
|
|
}
|
|
socket.on('data', (bytes) => {
|
|
deliver(Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes))
|
|
})
|
|
const swapDelivery = (next: (bytes: Buffer) => void): Buffer[] => {
|
|
deliver = next
|
|
return earlyData
|
|
}
|
|
const fail = (error: Error): void => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
clearTimeout(timeout)
|
|
socket.destroy()
|
|
reject(error)
|
|
}
|
|
const timeout = setTimeout(
|
|
() => fail(new Error('browser_local_route_connect_timeout')),
|
|
options.connectTimeoutMs ?? BROWSER_NETWORK_TUNNEL_CONNECT_TIMEOUT_MS
|
|
)
|
|
socket.on('error', (error) => {
|
|
if (!settled) {
|
|
fail(error)
|
|
}
|
|
})
|
|
socket.on('close', () => {
|
|
if (!settled) {
|
|
fail(new Error('browser_local_route_connect_closed'))
|
|
}
|
|
})
|
|
socket.on('connect', () => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
clearTimeout(timeout)
|
|
resolve(wrapConnectedSocket(socket, swapDelivery))
|
|
})
|
|
})
|
|
}
|
|
|
|
function wrapConnectedSocket(
|
|
socket: BrowserNetworkTunnelSocket,
|
|
swapDelivery: (next: (bytes: Buffer) => void) => Buffer[]
|
|
): Duplex {
|
|
const duplex = new Duplex({
|
|
read: () => {
|
|
socket.resume()
|
|
},
|
|
write: (chunk: Buffer, _encoding, callback) => {
|
|
socket.write(chunk, () => callback())
|
|
},
|
|
final: (callback) => {
|
|
socket.end()
|
|
callback()
|
|
},
|
|
destroy: (error, callback) => {
|
|
socket.destroy()
|
|
callback(error)
|
|
}
|
|
})
|
|
const buffered = swapDelivery((bytes) => {
|
|
if (!duplex.push(bytes)) {
|
|
socket.pause()
|
|
}
|
|
})
|
|
for (const bytes of buffered.splice(0)) {
|
|
if (!duplex.push(bytes)) {
|
|
socket.pause()
|
|
}
|
|
}
|
|
socket.on('end', () => {
|
|
duplex.push(null)
|
|
})
|
|
socket.on('error', (error) => {
|
|
duplex.destroy(error)
|
|
})
|
|
socket.on('close', () => {
|
|
duplex.destroy()
|
|
})
|
|
return duplex
|
|
}
|