@foxxie/centra
Version:
A fork of Centra to support shortcut methods in Foxxie, and with typescript support.
227 lines (177 loc) • 5.81 kB
text/typescript
import { join } from 'node:path';
import { RequestOptions, request } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { stringify } from 'querystring';
import { createGunzip, createInflate } from 'zlib';
import { version, name } from '../package.json';
import { HttpMethod, ResOptions } from '../index';
import { CentraResponse } from './CentraResponse';
const supportedCompressions = ['gzip', 'deflate'];
export class CentraRequest {
public url: URL;
public method: HttpMethod;
public data: string | Buffer | null;
public sendDataAs: 'form' | 'json' | 'buffer' | null;
public reqHeaders: Record<string, any>;
public streamEnabled: boolean;
public compressionEnabled: boolean;
public timeoutTime: number | null;
public ua: string;
public coreOptions: RequestOptions;
public resOptions: ResOptions;
public constructor(url: string | URL, method: HttpMethod = 'GET') {
this.url = typeof url === 'string' ? new URL(url) : url;
this.method = method;
this.data = null;
this.sendDataAs = null;
this.reqHeaders = {};
this.streamEnabled = false;
this.compressionEnabled = false;
this.timeoutTime = null;
this.ua = `${name}@${version}`;
this.coreOptions = {};
this.resOptions = {
maxBuffer: 50 * 1000000 // 50 MB
};
return this;
}
public query(a1: string | Record<string, any>, a2?: any): this {
if (typeof a1 === 'object') {
Object.keys(a1).forEach(queryKey => {
this.url.searchParams.append(queryKey, a1[queryKey]);
});
} else this.url.searchParams.append(a1, a2);
return this;
}
public path(relativePath: string): this {
this.url.pathname = join(this.url.pathname, relativePath);
return this;
}
public body(data: any, sendAs?: 'json' | 'buffer' | 'form'): this {
this.sendDataAs = typeof data === 'object' && !sendAs && !Buffer.isBuffer(data) ? 'json' : sendAs ? (sendAs.toLowerCase() as typeof sendAs) : 'buffer';
this.data = this.sendDataAs === 'form' ? stringify(data) : this.sendDataAs === 'json' ? JSON.stringify(data) : data;
return this;
}
public header(a1: string | Record<string, string>, a2?: string): this {
if (typeof a1 === 'object') {
Object.keys(a1).forEach(headerName => {
this.reqHeaders[headerName.toLowerCase()] = a1[headerName];
});
} else this.reqHeaders[a1.toLowerCase()] = a2;
return this;
}
public timeout(timeout: number): this {
this.timeoutTime = timeout;
return this;
}
public option<T extends keyof RequestOptions>(name: T, value: RequestOptions[T]): this {
this.coreOptions[name] = value;
return this;
}
public agent(agent: string): this {
this.ua = agent;
return this;
}
public stream(): this {
this.streamEnabled = true;
return this;
}
public compress(): this {
this.compressionEnabled = true;
if (!this.reqHeaders['accept-encoding']) this.reqHeaders['accept-encoding'] = supportedCompressions.join(', ');
return this;
}
public async json<T = any>(): Promise<T> {
const result = await this.send();
return result.json();
}
public async raw(): Promise<Buffer> {
const result = await this.send();
return result.body as Buffer;
}
public async text(): Promise<string> {
const result = await this.send();
return result.text();
}
public send(): Promise<CentraResponse> {
return new Promise((resolve, reject) => {
if (this.data) {
if (!this.reqHeaders.hasOwnProperty('content-type')) {
if (this.sendDataAs === 'json') {
this.reqHeaders['content-type'] = 'application/json';
} else if (this.sendDataAs === 'form') {
this.reqHeaders['content-type'] = 'application/x-www-form-urlencoded';
}
}
if (!this.reqHeaders.hasOwnProperty('content-length')) {
this.reqHeaders['content-length'] = Buffer.byteLength(this.data);
}
}
this.header('User-Agent', this.ua);
const options = Object.assign(
{
protocol: this.url.protocol,
host: this.url.hostname,
port: this.url.port,
path: this.url.pathname + (this.url.search === null ? '' : this.url.search),
method: this.method,
headers: this.reqHeaders
},
this.coreOptions
);
let req;
const resHandler = res => {
let stream = res;
if (this.compressionEnabled) {
if (res.headers['content-encoding'] === 'gzip') {
stream = res.pipe(createGunzip());
} else if (res.headers['content-encoding'] === 'deflate') {
stream = res.pipe(createInflate());
}
}
let centraRes;
if (this.streamEnabled) {
resolve(stream);
} else {
centraRes = new CentraResponse(res, this.resOptions);
stream.on('error', err => {
reject(err);
});
stream.on('aborted', () => {
reject(new Error('Server aborted request'));
});
stream.on('data', chunk => {
centraRes._addChunk(chunk);
if (this.resOptions.maxBuffer !== null && centraRes.body.length > this.resOptions.maxBuffer) {
stream.destroy();
reject('Received a response which was longer than acceptable when buffering. (' + this.body.length + ' bytes)');
}
});
stream.on('end', () => {
resolve(centraRes);
});
}
};
if (this.url.protocol === 'http:') {
req = request(options, resHandler);
} else if (this.url.protocol === 'https:') {
req = httpsRequest(options, resHandler);
} else if (this.url.protocol === 'localhost:') {
req = request(options, resHandler);
} else throw new Error('Bad URL protocol: ' + this.url.protocol);
if (this.timeoutTime) {
req.setTimeout(this.timeoutTime, () => {
req.abort();
if (!this.streamEnabled) {
reject(new Error('Timeout reached'));
}
});
}
req.on('error', err => {
reject(err);
});
if (this.data) req.write(this.data);
req.end();
});
}
}