271 lines
6.1 KiB
JavaScript
271 lines
6.1 KiB
JavaScript
'use strict'
|
|
|
|
const assert = require('node:assert')
|
|
const { AsyncResource } = require('node:async_hooks')
|
|
const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors')
|
|
const util = require('../core/util')
|
|
const { addSignal, removeSignal } = require('./abort-signal')
|
|
|
|
function noop () {}
|
|
|
|
function getWritableError (stream) {
|
|
return stream.errored ?? stream.writableErrored ?? stream._writableState?.errored
|
|
}
|
|
|
|
function createPrematureCloseError () {
|
|
const err = new Error('Premature close')
|
|
err.code = 'ERR_STREAM_PREMATURE_CLOSE'
|
|
return err
|
|
}
|
|
|
|
function trackWritableLifecycle (stream, callback) {
|
|
let done = false
|
|
|
|
const cleanup = () => {
|
|
stream.removeListener('close', onClose)
|
|
stream.removeListener('error', onError)
|
|
stream.removeListener('finish', onFinish)
|
|
}
|
|
|
|
const finish = (err, fromErrorEvent = false) => {
|
|
if (done) {
|
|
return
|
|
}
|
|
|
|
done = true
|
|
cleanup()
|
|
callback(err, fromErrorEvent)
|
|
}
|
|
|
|
const onClose = () => {
|
|
const err = getWritableError(stream)
|
|
finish(err ?? (!stream.writableFinished ? createPrematureCloseError() : undefined))
|
|
}
|
|
|
|
const onError = (err) => finish(err, true)
|
|
const onFinish = () => finish()
|
|
|
|
stream.on('close', onClose)
|
|
stream.on('error', onError)
|
|
stream.on('finish', onFinish)
|
|
|
|
if (stream.closed) {
|
|
process.nextTick(onClose)
|
|
} else if (stream.writableFinished) {
|
|
process.nextTick(onFinish)
|
|
}
|
|
}
|
|
|
|
class StreamHandler extends AsyncResource {
|
|
constructor (opts, factory, callback) {
|
|
if (!opts || typeof opts !== 'object') {
|
|
throw new InvalidArgumentError('invalid opts')
|
|
}
|
|
|
|
const { signal, method, opaque, body, onInfo, responseHeaders } = opts
|
|
|
|
try {
|
|
if (typeof callback !== 'function') {
|
|
throw new InvalidArgumentError('invalid callback')
|
|
}
|
|
|
|
if (typeof factory !== 'function') {
|
|
throw new InvalidArgumentError('invalid factory')
|
|
}
|
|
|
|
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
|
|
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
|
|
}
|
|
|
|
if (method === 'CONNECT') {
|
|
throw new InvalidArgumentError('invalid method')
|
|
}
|
|
|
|
if (onInfo && typeof onInfo !== 'function') {
|
|
throw new InvalidArgumentError('invalid onInfo callback')
|
|
}
|
|
|
|
super('UNDICI_STREAM')
|
|
} catch (err) {
|
|
if (util.isStream(body)) {
|
|
util.destroy(body.on('error', noop), err)
|
|
}
|
|
throw err
|
|
}
|
|
|
|
this.responseHeaders = responseHeaders || null
|
|
this.opaque = opaque || null
|
|
this.factory = factory
|
|
this.callback = callback
|
|
this.res = null
|
|
this.abort = null
|
|
this.context = null
|
|
this.controller = null
|
|
this.trailers = null
|
|
this.body = body
|
|
this.onInfo = onInfo || null
|
|
|
|
if (util.isStream(body)) {
|
|
body.on('error', (err) => {
|
|
this.onResponseError(this.controller, err)
|
|
})
|
|
}
|
|
|
|
addSignal(this, signal)
|
|
}
|
|
|
|
onRequestStart (controller, context) {
|
|
if (this.reason) {
|
|
controller.abort(this.reason)
|
|
return
|
|
}
|
|
|
|
assert(this.callback)
|
|
|
|
this.controller = controller
|
|
this.abort = (reason) => controller.abort(reason)
|
|
this.context = context
|
|
}
|
|
|
|
onResponseStart (controller, statusCode, headers, _statusMessage) {
|
|
const { factory, opaque, context, responseHeaders } = this
|
|
|
|
const rawHeaders = controller?.rawHeaders
|
|
const responseHeaderData = responseHeaders === 'raw'
|
|
? util.parseRawHeaders(rawHeaders)
|
|
: headers
|
|
|
|
if (statusCode < 200) {
|
|
if (this.onInfo) {
|
|
this.onInfo({ statusCode, headers: responseHeaderData })
|
|
}
|
|
return
|
|
}
|
|
|
|
this.factory = null
|
|
|
|
if (factory === null) {
|
|
return
|
|
}
|
|
|
|
const res = this.runInAsyncScope(factory, null, {
|
|
statusCode,
|
|
headers: responseHeaderData,
|
|
opaque,
|
|
context
|
|
})
|
|
|
|
if (
|
|
!res ||
|
|
typeof res.write !== 'function' ||
|
|
typeof res.end !== 'function' ||
|
|
typeof res.on !== 'function'
|
|
) {
|
|
throw new InvalidReturnValueError('expected Writable')
|
|
}
|
|
|
|
trackWritableLifecycle(res, (err, fromErrorEvent) => {
|
|
const { callback, res, opaque, trailers, abort } = this
|
|
|
|
this.res = null
|
|
if (err || !res?.readable) {
|
|
util.destroy(res, fromErrorEvent ? undefined : err)
|
|
}
|
|
|
|
this.callback = null
|
|
this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
|
|
|
|
if (err) {
|
|
abort(err)
|
|
}
|
|
})
|
|
|
|
res.on('drain', () => controller.resume())
|
|
|
|
this.res = res
|
|
|
|
const needDrain = res.writableNeedDrain !== undefined
|
|
? res.writableNeedDrain
|
|
: res._writableState?.needDrain
|
|
|
|
if (needDrain === true) {
|
|
controller.pause()
|
|
}
|
|
}
|
|
|
|
onResponseData (controller, chunk) {
|
|
const { res } = this
|
|
|
|
if (!res) {
|
|
return
|
|
}
|
|
|
|
if (res.write(chunk) === false) {
|
|
controller.pause()
|
|
}
|
|
}
|
|
|
|
onResponseEnd (_controller, trailers) {
|
|
const { res } = this
|
|
|
|
removeSignal(this)
|
|
|
|
if (!res) {
|
|
return
|
|
}
|
|
|
|
if (trailers && typeof trailers === 'object') {
|
|
this.trailers = trailers
|
|
}
|
|
|
|
res.end()
|
|
}
|
|
|
|
onResponseError (_controller, err) {
|
|
const { res, callback, opaque, body } = this
|
|
|
|
removeSignal(this)
|
|
|
|
this.factory = null
|
|
|
|
if (res) {
|
|
this.res = null
|
|
util.destroy(res, err)
|
|
} else if (callback) {
|
|
this.callback = null
|
|
queueMicrotask(() => {
|
|
this.runInAsyncScope(callback, null, err, { opaque })
|
|
})
|
|
}
|
|
|
|
if (body) {
|
|
this.body = null
|
|
util.destroy(body, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
function stream (opts, factory, callback) {
|
|
if (callback === undefined) {
|
|
return new Promise((resolve, reject) => {
|
|
stream.call(this, opts, factory, (err, data) => {
|
|
return err ? reject(err) : resolve(data)
|
|
})
|
|
})
|
|
}
|
|
|
|
try {
|
|
const handler = new StreamHandler(opts, factory, callback)
|
|
|
|
this.dispatch(opts, handler)
|
|
} catch (err) {
|
|
if (typeof callback !== 'function') {
|
|
throw err
|
|
}
|
|
const opaque = opts?.opaque
|
|
queueMicrotask(() => callback(err, { opaque }))
|
|
}
|
|
}
|
|
|
|
module.exports = stream
|