68 lines
1.7 KiB
TypeScript
68 lines
1.7 KiB
TypeScript
import { Readable } from 'node:stream'
|
|
|
|
export interface NodeReadableToWebStreamOptions {
|
|
onCancel?: (reason?: unknown) => void | Promise<void>
|
|
onComplete?: () => void
|
|
}
|
|
|
|
/**
|
|
* 将 Node.js Readable 转换为受 Web Stream 消费速度约束的流。
|
|
*
|
|
* 每次 pull 只向下游交付一个分块,避免 data 事件持续读取大文件、
|
|
* 而浏览器或代理来不及接收时在 Node 进程内堆积大量内存。
|
|
*/
|
|
export function nodeReadableToWebStream(
|
|
source: Readable,
|
|
options: NodeReadableToWebStreamOptions = {},
|
|
): ReadableStream<Uint8Array> {
|
|
const iterator = source[Symbol.asyncIterator]()
|
|
let finished = false
|
|
|
|
return new ReadableStream<Uint8Array>(
|
|
{
|
|
async pull(controller) {
|
|
if (finished) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const next = await iterator.next()
|
|
if (next.done) {
|
|
finished = true
|
|
controller.close()
|
|
try {
|
|
options.onComplete?.()
|
|
} catch (error) {
|
|
console.error('[node-readable-stream] completion callback failed:', error)
|
|
}
|
|
return
|
|
}
|
|
|
|
const chunk = next.value
|
|
controller.enqueue(chunk instanceof Uint8Array ? chunk : Buffer.from(chunk))
|
|
} catch (error) {
|
|
finished = true
|
|
if (!source.destroyed) {
|
|
source.destroy()
|
|
}
|
|
controller.error(error)
|
|
}
|
|
},
|
|
async cancel(reason) {
|
|
finished = true
|
|
try {
|
|
await options.onCancel?.(reason)
|
|
} finally {
|
|
if (!source.destroyed) {
|
|
source.destroy()
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
highWaterMark: 1,
|
|
size: () => 1,
|
|
},
|
|
)
|
|
}
|