0xweb
Version:
Contract package manager and other web3 tools
220 lines (179 loc) • 6.66 kB
text/typescript
import { class_Uri } from 'atma-utils';
import { $path } from './$path';
import { $require } from './$require';
import { $logger } from './$logger';
import { Directory, File } from 'atma-io';
import { $is } from './$is';
interface THttpResponse<T> {
status: 200 | number
data: T
}
export namespace $http {
const handlers = [] as {
rgx: RegExp,
handler: (opts: IHttpFetch) => Promise<{ status, data, headers? }>
}[];
export function register (rgx: RegExp, handler: (typeof handlers[0]['handler'])) {
handlers.push({ rgx, handler });
}
export interface IHttpFetch {
url: string
method?: 'GET' | string
params?: Record<string, string>
body?: string | Record<string, string> | any
headers?: Record<string, string>
responseType?: 'stream'
}
async function doFetch <T = any> (opts: IHttpFetch): Promise<THttpResponse<T>> {
let url = opts.url;
if (opts.params) {
let c = url.includes('?') ? '&' : '?';
url += c + new URLSearchParams(opts.params).toString();
}
let headers = new Headers(opts.headers ?? {});
let body = opts.body;
if (body != null) {
let contentType = headers.get('Content-Type');
if (contentType == null) {
contentType = 'application/json';
headers.append('Content-Type', contentType);
}
if (typeof body !== 'string') {
if (contentType.includes('urlencoded')) {
const params = new URLSearchParams(opts.body);
body = params.toString();
}
if (contentType.includes('json')) {
body = JSON.stringify(body);
}
}
}
let options = {
method: opts.method ?? 'GET',
headers: headers,
body: body
};
let handler = handlers.find(x => x.rgx.test(url));
if (handler) {
return handler.handler(opts);
}
let resp = await doFetchInner(url, options);
let contentType = resp.headers.get('Content-Type');
let data: any = resp.body;
if (opts.responseType !== 'stream') {
if (contentType?.includes('json')) {
data = await resp.json();
} else {
data = await resp.text();
}
}
let response = {
status: resp.status,
data: data
};
if (resp.status > 400) {
let err = new HttpError({
url,
method: options.method
}, response);
throw err;
}
return response;
}
async function doFetchInner (url: string, reqInit: RequestInit, options?: {
retries?: number
}) {
options ??= {};
options.retries ??= 3;
let resp: Response;
let timeout = setTimeout(() => {
$logger.log(`${reqInit.method} ${url} response takes longer as expected. Waiting...`);
}, 8_000);
try {
resp = await fetch(url, reqInit);
} catch (error) {
let errCode = error.cause?.code;
let message = `Fetch failed for ${ url } with "${error.message }: ${errCode}"`;
if (--options.retries < 0) {
throw new Error(message);
}
console.log(`Retry HTTP request after "${message}"`);
return doFetchInner(url, reqInit, options);
} finally {
clearTimeout(timeout);
}
return resp;
}
export function get<T = any> (opts: IHttpFetch): Promise<THttpResponse<T>>
export function get<T = any> (url: string): Promise<THttpResponse<T>>
export function get<T = any> (mix: string | IHttpFetch): Promise<THttpResponse<T>> {
let opts = typeof mix === 'string' ? { url: mix } : mix;
return doFetch<T>({
...opts,
method: 'GET'
});
}
export function post<T = any> (opts: IHttpFetch) {
return doFetch<T>({
...opts,
method: 'POST'
});
}
/**
* output: Directory or File
*/
export async function download(url: string, config: IHttpFetch & { output: string }) {
let output = $require.notNull(config.output, `Output is undefined. Should be directory or file path`);
if ($path.hasExt(output) === false) {
let filename = new class_Uri(url).file;
$require.notEmpty(filename, `There is no filename with extension in source url. To save a file, you must specify the filename in "output"`);
output = class_Uri.combine(output, filename);
}
return $is.NODE
? $httpNode.download(url, output, config)
: $httpBrowser.download(url, output, config)
;
}
class HttpError extends Error {
constructor (public req: { method, url }, public response: { status: number, data: any}) {
super(`HTTP Error ${response.status} for ${req.method}:${req.url}. ${typeof response.data === 'string' ? response.data : ''}`);
}
}
}
namespace $httpNode {
/**
* output: Directory or File
*/
export async function download(url: string, output: string, config?: RequestInit){
if ($path.isAbsolute(output) === false) {
output = class_Uri.combine(`file://`, process.cwd(), output);
}
const fileUri = new class_Uri(output);
const filepath = fileUri.toLocalFile();
await Directory.ensureAsync(fileUri.toDir());
const response = await fetch (url, config);
const buffer = await response.arrayBuffer();
await File.writeAsync(output, buffer, { skipHooks: true})
}
}
export namespace $httpBrowser {
/**
* output: Directory or File
*/
export async function download(url: string, output: string, config?: RequestInit){
if ($path.isAbsolute(output) === false) {
output = class_Uri.combine(`file://`, process.cwd(), output);
}
const fileUri = new class_Uri(output);
const filepath = fileUri.toLocalFile();
await Directory.ensureAsync(fileUri.toDir());
const response = await fetch (url, config);
const blob = await response.blob();
const uri = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = uri;
a.download = output;
document.body.appendChild(a);
a.click();
}
}