import {Request} from "./Request"; import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "@extollo/util" import {ServerResponse} from "http" export class HeadersAlreadySentError extends ErrorWithContext { constructor(response: Response, headerName?: string) { super(`Cannot modify or re-send headers for this request as they have already been sent.`); this.context = { headerName } } } export class ResponseAlreadySentError extends ErrorWithContext { constructor(response: Response) { super(`Cannot modify or re-send response as it has already ended.`); } } export class Response { private headers: {[key: string]: string | string[]} = {} private _sentHeaders: boolean = false private _responseEnded: boolean = false private _status: HTTPStatus = HTTPStatus.OK public body: string = '' public readonly sending$: BehaviorSubject = new BehaviorSubject() public readonly sent$: BehaviorSubject = new BehaviorSubject() constructor( public readonly request: Request, protected readonly serverResponse: ServerResponse, ) { } public getStatus() { return this._status } public setStatus(status: HTTPStatus) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status') this._status = status } public get cookies() { return this.request.cookies } public getHeader(name: string): string | string[] | undefined { return this.headers[name] } public setHeader(name: string, value: string | string[]) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name) this.headers[name] = value return this } public setHeaders(data: {[name: string]: string | string[]}) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this) this.headers = {...this.headers, ...data} return this } public appendHeader(name: string, value: string | string[]) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name) if ( !Array.isArray(value) ) value = [value] let existing = this.headers[name] ?? [] if ( !Array.isArray(existing) ) existing = [existing] existing = [...existing, ...value] if ( existing.length === 1 ) existing = existing[0] this.headers[name] = existing } public sendHeaders() { const headers = {} as any const setCookieHeaders = this.cookies.getSetCookieHeaders() if ( setCookieHeaders.length ) headers['Set-Cookie'] = setCookieHeaders for ( const key in this.headers ) { if ( !this.headers.hasOwnProperty(key) ) continue headers[key] = this.headers[key] } this.serverResponse.writeHead(this._status, headers) this._sentHeaders = true } public hasSentHeaders() { return this._sentHeaders } public async write(data: any) { return new Promise((res, rej) => { if ( !this._sentHeaders ) this.sendHeaders() this.serverResponse.write(data, error => { if ( error ) rej(error) else res() }) }) } public async send() { await this.sending$.next(this) this.setHeader('Content-Length', String(this.body?.length ?? 0)) await this.write(this.body ?? '') this.end() await this.sent$.next(this) } public end() { if ( this._responseEnded ) throw new ResponseAlreadySentError(this) this._sentHeaders = true this.serverResponse.end() } // location? }