import { EventEmitter } from "node:events";
import VarBuffer from "../src/consumers/VarBuffer.js";
import { makeDeferred } from "@cern/nodash";
import { Readable } from "node:stream";
const debug = d("cam:part");
const SearchStates = { HEADER_END: 1, CONTENT_END: 2, NEXT_BOUNDARY: 3 };
const CRLFx2 = "\r\n\r\n";
class MultiPart extends EventEmitter {
#buffer = new VarBuffer();
#state = SearchStates.HEADER_END;
#deferred = makeDeferred();
get promise() { return this.#deferred.promise; }
if (!(res?.data instanceof Readable)) {
throw new Error("MultiPart must be used on a `stream` request from axios");
this.#boundary = this.#getBoundary(res?.headers?.["content-type"] ?? "") ??
debug("part parser constructed");
const addfunc = this.#add.bind(this);
const readable = res.data;
.once("error", (err) => {
if (this.#deferred.isPending) {
this.#deferred.reject(err);
debug("part reader error");
readable.removeAllListeners();
if (this.#deferred.isPending) {
this.#deferred.resolve(null);
debug("part reader closed");
#getBoundary(contentType) {
const match = contentType
.match(/boundary="?([\w\s'()+,\-./:=?]{1,70})"?;?/);
if (!match) { return undefined; }
return match[1].trimEnd();
while (this.#buffer.length - this.#offset > this.#boundary.length + 2) {
case SearchStates.HEADER_END:
index = this.#buffer.buffer().indexOf(CRLFx2, this.#offset);
const header = this.#buffer.slice(0, index).toString("ascii");
const match = header.match(/Content-Length:\s*(\d+)/i);
this.#offset = index + CRLFx2.length;
(header.match(/Content-Type:(.*)/i)?.[1] ?? "").trim();
this.#frameEnd = this.#offset + Number(match[1]);
this.#state = SearchStates.CONTENT_END;
this.#state = SearchStates.NEXT_BOUNDARY;
else if (this.#buffer.length > this.MaxHeaderLength) {
this.#buffer.shift(this.#buffer.length - (CRLFx2.length - 1));
this.#offset = this.#buffer.length;
case SearchStates.CONTENT_END:
if (this.#buffer.length < this.#frameEnd) {
this.emit("part", this.#contentType,
this.#buffer.slice(this.#offset, this.#frameEnd));
this.#buffer.shift(this.#frameEnd);
this.#state = SearchStates.HEADER_END;
case SearchStates.NEXT_BOUNDARY:
index = this.#buffer.buffer().indexOf(this.#boundary, this.#offset);
this.#buffer.shift(index + this.#boundary.length);
this.#state = SearchStates.HEADER_END;
else if (this.#buffer.length > this.MaxHeaderLength) {
this.#buffer.shift(this.#buffer.length - (this.#boundary.length - 1));
this.#offset = Math.max(0,
this.#buffer.length - (this.#boundary.length - 1));
this.#state = SearchStates.HEADER_END;
export default MultiPart;