/** * Node.js streams adapter for the low-level multipart parser. * * @since 4.0.0 */ // oxlint-disable typescript/no-unsafe-declaration-merging /// import type { BaseConfig, MultipartError, Parser, PartInfo } from "effect/http/MultipartParser" import { make as makeParser } from "effect/http/MultipartParser" import type { IncomingHttpHeaders } from "node:http" import { Duplex, Readable } from "node:stream" /** * A part emitted by the Node.js multipart parser. * * @category models * @since 4.0.0 */ export type Part = Field | FileStream /** * A parsed multipart field. * * @category models * @since 4.0.0 */ export interface Field { readonly _tag: "Field" readonly info: PartInfo readonly value: Uint8Array } /** * A Node.js duplex stream that emits parsed multipart parts. * * @category models * @since 4.0.0 */ export interface MultipartStream extends Duplex { [Symbol.asyncIterator](): NodeJS.AsyncIterator on(event: "field", listener: (field: Field) => void): this on(event: "file", listener: (file: FileStream) => void): this on(event: "close", listener: () => void): this on(event: "data", listener: (part: Part) => void): this on(event: "drain", listener: () => void): this on(event: "end", listener: () => void): this on(event: "error", listener: (err: MultipartError) => void): this on(event: "finish", listener: () => void): this on(event: "pause", listener: () => void): this on(event: "pipe", listener: (src: Readable) => void): this on(event: "readable", listener: () => void): this on(event: "resume", listener: () => void): this on(event: "unpipe", listener: (src: Readable) => void): this on(event: string | symbol, listener: (...args: Array) => void): this read(size?: number): Part | null } /** * Configuration for the Node.js multipart parser. * * @category models * @since 4.0.0 */ export type NodeConfig = Omit & { readonly headers: IncomingHttpHeaders } /** * A Node.js duplex stream that parses multipart input. * * @category models * @since 4.0.0 */ export class MultipartStream extends Duplex { private _parser: Parser _canWrite = true private _writeCallback: (() => void) | undefined constructor(config: NodeConfig) { super({ readableObjectMode: true }) let currentError: MultipartError | undefined let currentFile: FileStream | undefined this._parser = makeParser({ ...(config as any), onField: (info, value) => { if (currentError !== undefined) return const field: Field = { _tag: "Field", info, value } this.push(field) this.emit("field", field) }, onFile: (info) => { if (currentError !== undefined) return (_) => {} const file = new FileStream(info, this) currentFile = file this.push(file) this.emit("file", file) return (chunk) => { if (currentError !== undefined) return this._canWrite = file.push(chunk) if (chunk === null && !this._canWrite) { currentFile = undefined this._resume() } } }, onError: (error) => { this.emit("error", error) currentFile?.emit("error", error) currentError = error }, onDone: () => { this.push(null) } }) } _resume() { this._canWrite = true if (this._writeCallback !== undefined) { const callback = this._writeCallback this._writeCallback = undefined callback() } } override _read(_size: number) {} override _write( chunk: any, encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void ): void { this._parser.write( chunk instanceof Uint8Array ? chunk : Buffer.from(chunk, encoding) ) if (this._canWrite) { callback() } else { this._writeCallback = callback } } override _final(callback: (error?: Error | null | undefined) => void): void { this._parser.end() callback() } } /** * Creates a Node.js multipart parser stream. * * @category constructors * @since 4.0.0 */ export const make = (config: NodeConfig): MultipartStream => new MultipartStream(config) /** * A readable stream containing a parsed multipart file. * * @category models * @since 4.0.0 */ export class FileStream extends Readable { readonly _tag = "File" readonly filename: string | undefined readonly info: PartInfo private _parent: MultipartStream constructor( info: PartInfo, parent: MultipartStream ) { super() this.info = info this._parent = parent this.filename = info.filename } override _read(_size: number) { if (this._parent._canWrite === false) { this._parent._resume() } } }