diff --git a/dist/index.js b/dist/index.js index 53e3937..5ca4676 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,106 +3,108 @@ import https from "https"; import { URL } from "url"; import querystring from "querystring"; import zlib from "zlib"; -const parseCookies = (cookieHeader) => { - const cookies = {}; - if (cookieHeader) { +class fetch { + cookies = {}; + constructor() { } + parseCookies(cookieHeader) { + if (!cookieHeader) + return; cookieHeader.split(";").forEach(cookie => { const [key, value] = cookie.split("="); if (key && value) - cookies[key.trim()] = value.trim(); + this.cookies[key.trim()] = value.trim(); }); } - return cookies; -}; -const formatCookies = (cookies) => Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; "); -const addCookiesToRequest = (cookies, headers) => { - headers["Cookie"] = formatCookies(cookies); -}; -const decompress = (data, encoding) => { - return encoding === "br" ? zlib.brotliDecompressSync(data) : - encoding === "gzip" ? zlib.gunzipSync(data) : - encoding === "deflate" ? zlib.inflateSync(data) : - data; -}; -const readData = (res, mode) => new Promise((resolve, reject) => { - const chunks = []; - res - .on("data", chunk => chunks.push(chunk)) - .on("end", () => { - try { - const data = decompress(Buffer.concat(chunks), res.headers["content-encoding"]); - resolve((mode === "json" ? JSON.parse(data.toString("utf8")) : - mode === "buffer" ? data : - mode === "arraybuffer" ? new Uint8Array(data).buffer : - data.toString("utf8"))); - } - catch (err) { - reject(err); - } - }) - .on("error", reject); -}); -const fetch = async (urlString, options = {}, redirectCount = 0, cookies = {}) => { - const { protocol, hostname, pathname, search, port } = new URL(urlString); - options.followRedirects = options.followRedirects ?? true; - if (options.signal?.aborted) - throw new Error("Request aborted"); - const body = options.method === "POST" && options.body - ? options.headers?.["Content-Type"] === "application/json" - ? JSON.stringify(options.body) - : querystring.stringify(options.body) - : null; - const requestOptions = { - hostname, - port: port || (protocol === "https:" ? 443 : 80), - path: pathname + search, - method: options.method || "GET", - headers: { - ...options.headers, - ...(body ? { "Content-Length": Buffer.byteLength(body) } : {}), - "Accept-Encoding": "br, gzip, deflate" - } - }; - addCookiesToRequest(cookies, requestOptions.headers); - return new Promise((resolve, reject) => { - const requester = protocol === "https:" ? https : http; - const req = requester.request(requestOptions, res => { - if (res.headers["set-cookie"]) - cookies = parseCookies(res.headers["set-cookie"].join("; ")); - if (options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - if (redirectCount >= (options.maxRedirects || 5)) - return reject(new Error("Max redirects exceeded")); - if (!res.headers.location) - return reject(new Error("Redirect location not provided")); - const nextUrl = new URL(res.headers.location, urlString); - return resolve(fetch(nextUrl.toString(), options, redirectCount + 1, cookies)); + formatCookies() { + return Object.entries(this.cookies) + .map(([key, value]) => `${key}=${value}`) + .join("; "); + } + decompress(data, encoding) { + return encoding === "br" ? zlib.brotliDecompressSync(data) : + encoding === "gzip" ? zlib.gunzipSync(data) : + encoding === "deflate" ? zlib.inflateSync(data) : + data; + } + readData(res, mode) { + return new Promise((resolve, reject) => { + const chunks = []; + res + .on("data", chunk => chunks.push(chunk)) + .on("end", () => { + try { + const data = this.decompress(Buffer.concat(chunks), res.headers["content-encoding"]); + resolve(mode === "json" ? JSON.parse(data.toString("utf8")) : + mode === "buffer" ? data : + mode === "arraybuffer" ? new Uint8Array(data).buffer : + data.toString("utf8")); + } + catch (err) { + reject(err); + } + }) + .on("error", reject); + }); + } + async fetch(urlString, options = {}, redirectCount = 0) { + options.followRedirects = options.followRedirects ?? true; + const { protocol, hostname, pathname, search, port } = new URL(urlString); + const body = options.method === "POST" && options.body + ? options.headers?.["Content-Type"] === "application/json" + ? JSON.stringify(options.body) + : querystring.stringify(options.body) + : null; + const requestOptions = { + hostname, + port: port || (protocol === "https:" ? 443 : 80), + path: pathname + search, + method: options.method || "GET", + headers: { + ...options.headers, + ...(body ? { "Content-Length": Buffer.byteLength(body) } : {}), + "Cookie": this.formatCookies(), + "Accept-Encoding": "br, gzip, deflate", } - if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) - return reject(new Error(`Request failed with status code ${res.statusCode}`)); - resolve({ - body: res, - headers: res.headers, - cookies: cookies, - text: () => readData(res, "text"), - json: () => readData(res, "json"), - buffer: () => readData(res, "buffer"), - arrayBuffer: () => readData(res, "arraybuffer") + }; + return new Promise((resolve, reject) => { + const requester = protocol === "https:" ? https : http; + const req = requester.request(requestOptions, res => { + this.parseCookies(res.headers["set-cookie"]?.join("; ")); + if (options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + req.destroy(); + if (redirectCount >= (options.maxRedirects || 5)) + return reject(new Error("Max redirects exceeded")); + if (!res.headers.location) + return reject(new Error("Redirect location not provided")); + const nextUrl = new URL(res.headers.location, urlString); + return resolve(this.fetch(nextUrl.toString(), options, redirectCount + 1)); + } + return resolve({ + body: res, + headers: res.headers, + cookies: this.cookies, + text: () => this.readData(res, "text"), + json: () => this.readData(res, "json"), + buffer: () => this.readData(res, "buffer"), + arrayBuffer: () => this.readData(res, "arraybuffer"), + }); }); - }); - req.setTimeout(options.timeout || 5e3, () => { - req.destroy(); - reject(new Error("Request timed out")); - }); - req.on("error", reject); - if (options.signal) { - options.signal.addEventListener("abort", () => { - req.destroy(new Error("Request aborted")); - reject(new Error("Request aborted")); + req.setTimeout(options.timeout || 5e3, () => { + req.destroy(); + reject(new Error("Request timed out")); }); - } - if (body) - req.write(body); - req.end(); - }); -}; -export default fetch; + req.on("error", reject); + if (options.signal) { + options.signal.addEventListener("abort", () => { + req.destroy(new Error("Request aborted")); + reject(new Error("Request aborted")); + }); + } + if (body) + req.write(body); + req.end(); + }); + } +} +const inst = new fetch(); +export default inst.fetch.bind(inst); diff --git a/src/index.ts b/src/index.ts index 780cf9f..44d9bd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,162 +4,150 @@ import { URL } from "url"; import querystring from "querystring"; import zlib from "zlib"; -type CookieStorage = Record; +class fetch { + private cookies: Record = {}; -type ResponseData = { - body: http.IncomingMessage; - headers: http.IncomingHttpHeaders; - cookies: CookieStorage; - text: () => Promise; - json: () => Promise; - buffer: () => Promise; - arrayBuffer: () => Promise; -}; + constructor() { } -interface ExtendedRequestOptions extends http.RequestOptions { - body?: any; - timeout?: number; - followRedirects?: boolean; - maxRedirects?: number; - signal?: AbortSignal; -} + private parseCookies(cookieHeader: string | undefined): void { + if(!cookieHeader) + return; -const parseCookies = (cookieHeader: string | undefined): CookieStorage => { - const cookies: CookieStorage = {}; - if(cookieHeader) { cookieHeader.split(";").forEach(cookie => { const [key, value] = cookie.split("="); if(key && value) - cookies[key.trim()] = value.trim(); + this.cookies[key.trim()] = value.trim(); }); } - return cookies; -}; -const formatCookies = (cookies: CookieStorage): string => - Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; "); + private formatCookies(): string { + return Object.entries(this.cookies) + .map(([key, value]) => `${key}=${value}`) + .join("; "); + } -const addCookiesToRequest = (cookies: CookieStorage, headers: http.OutgoingHttpHeaders): void => { - headers["Cookie"] = formatCookies(cookies); -}; + private decompress( + data: Buffer, + encoding: string | undefined + ): Buffer { + return encoding === "br" ? zlib.brotliDecompressSync(data) : + encoding === "gzip" ? zlib.gunzipSync(data) : + encoding === "deflate" ? zlib.inflateSync(data) : + data; + } -const decompress = ( - data: Buffer, - encoding: string | undefined -): Buffer => { - return encoding === "br" ? zlib.brotliDecompressSync(data) : - encoding === "gzip" ? zlib.gunzipSync(data) : - encoding === "deflate" ? zlib.inflateSync(data) : - data; -}; + private readData( + res: http.IncomingMessage, + mode: "text" | "json" | "buffer" | "arraybuffer" + ): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + res + .on("data", chunk => chunks.push(chunk)) + .on("end", () => { + try { + const data = this.decompress(Buffer.concat(chunks), res.headers["content-encoding"]); + resolve( + mode === "json" ? JSON.parse(data.toString("utf8")) : + mode === "buffer" ? data : + mode === "arraybuffer" ? new Uint8Array(data).buffer : + data.toString("utf8") + ); + } + catch(err: any) { + reject(err); + } + }) + .on("error", reject); + }); + } -const readData = ( - res: http.IncomingMessage, - mode: "text" | "json" | "buffer" | "arraybuffer" -): Promise => - new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - res - .on("data", chunk => chunks.push(chunk)) - .on("end", () => { - try { - const data = decompress(Buffer.concat(chunks), res.headers["content-encoding"]); - resolve(( - mode === "json" ? JSON.parse(data.toString("utf8")) : - mode === "buffer" ? data : - mode === "arraybuffer" ? new Uint8Array(data).buffer : - data.toString("utf8") - )) as T; + async fetch( + urlString: string, + options: http.RequestOptions & { + body?: any; + timeout?: number; + followRedirects?: boolean; + maxRedirects?: number; + signal?: AbortSignal; + } = {}, + redirectCount = 0 + ): Promise<{ + body: http.IncomingMessage; + headers: http.IncomingHttpHeaders; + cookies: Record; + text: () => Promise; + json: () => Promise; + buffer: () => Promise; + arrayBuffer: () => Promise; + }> { + options.followRedirects = options.followRedirects ?? true; + + const { protocol, hostname, pathname, search, port } = new URL(urlString); + const body = options.method === "POST" && options.body + ? options.headers?.["Content-Type"] === "application/json" + ? JSON.stringify(options.body) + : querystring.stringify(options.body as querystring.ParsedUrlQueryInput) + : null; + const requestOptions: http.RequestOptions = { + hostname, + port: port || (protocol === "https:" ? 443 : 80), + path: pathname + search, + method: options.method || "GET", + headers: { + ...options.headers, + ...(body ? { "Content-Length": Buffer.byteLength(body) } : {}), + "Cookie": this.formatCookies(), + "Accept-Encoding": "br, gzip, deflate", + } + }; + + return new Promise((resolve, reject) => { + const requester = protocol === "https:" ? https : http; + const req = requester.request(requestOptions, res => { + this.parseCookies(res.headers["set-cookie"]?.join("; ")); + if(options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + req.destroy(); + if(redirectCount >= (options.maxRedirects || 5)) + return reject(new Error("Max redirects exceeded")); + + if(!res.headers.location) + return reject(new Error("Redirect location not provided")); + + const nextUrl = new URL(res.headers.location, urlString); + return resolve(this.fetch(nextUrl.toString(), options, redirectCount + 1)); } - catch(err: any) { - reject(err); - } - }) - .on("error", reject); - }); + return resolve({ + body: res, + headers: res.headers, + cookies: this.cookies, + text: () => this.readData(res, "text"), + json: () => this.readData(res, "json"), + buffer: () => this.readData(res, "buffer"), + arrayBuffer: () => this.readData(res, "arraybuffer"), + }); + }); -const fetch = async ( - urlString: string, - options: ExtendedRequestOptions = {}, - redirectCount: number = 0, - cookies: CookieStorage = {} -): Promise => { - const { protocol, hostname, pathname, search, port } = new URL(urlString); - options.followRedirects = options.followRedirects ?? true; + req.setTimeout(options.timeout || 5e3, () => { + req.destroy(); + reject(new Error("Request timed out")); + }); - if(options.signal?.aborted) - throw new Error("Request aborted"); + req.on("error", reject); - const body = - options.method === "POST" && options.body - ? options.headers?.["Content-Type"] === "application/json" - ? JSON.stringify(options.body) - : querystring.stringify(options.body as querystring.ParsedUrlQueryInput) - : null; - - const requestOptions: http.RequestOptions = { - hostname, - port: port || (protocol === "https:" ? 443 : 80), - path: pathname + search, - method: options.method || "GET", - headers: { - ...options.headers, - ...(body ? { "Content-Length": Buffer.byteLength(body) } : {}), - "Accept-Encoding": "br, gzip, deflate" - } - }; - - addCookiesToRequest(cookies, requestOptions.headers!); - - return new Promise((resolve, reject) => { - const requester = protocol === "https:" ? https : http; - const req = requester.request(requestOptions, res => { - if(res.headers["set-cookie"]) - cookies = parseCookies(res.headers["set-cookie"].join("; ")); - - if(options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - if(redirectCount >= (options.maxRedirects || 5)) - return reject(new Error("Max redirects exceeded")); - - if(!res.headers.location) - return reject(new Error("Redirect location not provided")); - - const nextUrl = new URL(res.headers.location, urlString); - return resolve(fetch(nextUrl.toString(), options, redirectCount + 1, cookies)); + if(options.signal) { + options.signal.addEventListener("abort", () => { + req.destroy(new Error("Request aborted")); + reject(new Error("Request aborted")); + }); } - if(res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) - return reject(new Error(`Request failed with status code ${res.statusCode}`)); - - resolve({ - body: res, - headers: res.headers, - cookies: cookies, - text: () => readData(res, "text"), - json: () => readData(res, "json"), - buffer: () => readData(res, "buffer"), - arrayBuffer: () => readData(res, "arraybuffer") - }); + if(body) + req.write(body); + req.end(); }); + } +} - req.setTimeout(options.timeout || 5e3, () => { - req.destroy(); - reject(new Error("Request timed out")); - }); - - req.on("error", reject); - - if(options.signal) { - options.signal.addEventListener("abort", () => { - req.destroy(new Error("Request aborted")); - reject(new Error("Request aborted")); - }); - } - - if(body) - req.write(body); - req.end(); - }); -}; - -export default fetch; +const inst = new fetch(); +export default inst.fetch.bind(inst);