-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
124 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,129 @@ | ||
export const defaultReqInit: RequestInit = { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}; | ||
|
||
export async function request<T>(uri: string, options: RequestInit = {}): Promise<T> { | ||
const res = await fetch(uri, { | ||
...defaultReqInit, | ||
...options, | ||
}); | ||
if (res.status === 0) throw new Error('Request fail'); | ||
if (res.status >= 500) throw new Error(res.statusText); | ||
try { | ||
const data = await res.json(); | ||
if (res.status >= 400) throw new (Error as any)(data?.message || res.statusText, { cause: data }); | ||
return data; | ||
} catch { | ||
throw new Error('Server error'); | ||
} | ||
} | ||
import { GemError } from '../lib/utils'; | ||
|
||
type ReqBody = Record<string, unknown> | BodyInit; | ||
type ReqParams = Record<string, string | number>; | ||
const TypedArray = Object.getPrototypeOf(Object.getPrototypeOf(new Int8Array())).constructor; | ||
|
||
function serializationBody(body?: ReqBody): BodyInit | undefined { | ||
if ( | ||
body instanceof FormData || | ||
body instanceof Blob || | ||
body instanceof TypedArray || | ||
body instanceof URLSearchParams || | ||
body instanceof ReadableStream || | ||
body instanceof ArrayBuffer | ||
) { | ||
return body as BodyInit; | ||
} | ||
if (typeof body === 'object') { | ||
return JSON.stringify(body); | ||
} | ||
return body; | ||
} | ||
type Options = { | ||
origin?: string; | ||
base?: string; | ||
// only for get/del/put/post | ||
appendHeaders?: (input?: any) => HeadersInit; | ||
transformInput?: (input?: any) => any; | ||
transformOutput?: (output: any, req: Request) => any; | ||
} & RequestInit; | ||
|
||
function serializationUrl(url: string, params?: Record<string, string | number>) { | ||
const { origin, pathname, search } = new URL(url, location.origin); | ||
const query = new URLSearchParams(params as Record<string, string>).toString(); | ||
return `${origin}${pathname}${search ? `${search}&` : query ? '?' : ''}${query}`; | ||
} | ||
function initRequest(options: Options = {}) { | ||
const { | ||
origin = location.origin, | ||
base = '', | ||
appendHeaders = () => ({}), | ||
transformInput = (p?: any) => p, | ||
transformOutput = (p?: any) => p, | ||
...reqInit | ||
} = options; | ||
|
||
export function get<T>(url: string, params?: Record<string, string | number>) { | ||
return request<T>(serializationUrl(url, params), { method: 'GET' }); | ||
} | ||
export function del<T>(url: string, params?: Record<string, string | number>) { | ||
return request<T>(serializationUrl(url, params), { method: 'DELETE' }); | ||
} | ||
export function put<T>(url: string, body?: ReqBody) { | ||
return request<T>(url, { method: 'PUT', body: serializationBody(body) }); | ||
} | ||
export function post<T>(url: string, body?: ReqBody) { | ||
return request<T>(url, { method: 'POST', body: serializationBody(body) }); | ||
const serializationBody = (body?: ReqBody): BodyInit | undefined => { | ||
if ( | ||
body instanceof FormData || | ||
body instanceof Blob || | ||
body instanceof TypedArray || | ||
body instanceof URLSearchParams || | ||
body instanceof ReadableStream || | ||
body instanceof ArrayBuffer | ||
) { | ||
return body as BodyInit; | ||
} | ||
if (typeof body === 'object') { | ||
return JSON.stringify(body); | ||
} | ||
return body; | ||
}; | ||
|
||
const serializationUrl = (path: string, params?: ReqParams) => { | ||
if (new URL(path, location.origin).pathname !== path) { | ||
throw new GemError('`path` format is incorrect'); | ||
} | ||
const query = new URLSearchParams(params as Record<string, string>).toString(); | ||
return `${origin}${base}${path}${query ? '?' : ''}${query}`; | ||
}; | ||
|
||
const serializationHeaders = (headers?: HeadersInit) => { | ||
if (!headers) return {}; | ||
// auto `toLowerCase` | ||
if (headers instanceof Headers) return Object.fromEntries(headers); | ||
const entries = Array.isArray(headers) ? headers : Object.entries(headers); | ||
return Object.fromEntries(entries.map(([key, value]) => [key.toLowerCase(), value])); | ||
}; | ||
|
||
const request = async <T>(uri: string, reqOptions: RequestInit = {}): Promise<T> => { | ||
const req = new Request(uri, { | ||
...reqInit, | ||
...reqOptions, | ||
headers: new Headers({ | ||
'content-type': 'application/json', | ||
...serializationHeaders(reqInit.headers), | ||
...serializationHeaders(reqOptions.headers), | ||
}), | ||
}); | ||
const res = await fetch(req); | ||
|
||
if (res.status === 0) throw new Error('Request fail'); | ||
if (res.status >= 500) throw new Error(res.statusText); | ||
|
||
const contentType = req.headers.get('content-type')!; | ||
const isClientError = res.status >= 400; | ||
try { | ||
let data: any; | ||
if (contentType.includes('json')) { | ||
data = await res.json(); | ||
} else if (contentType.includes('octet-stream')) { | ||
data = await res.arrayBuffer(); | ||
} else { | ||
data = await res.text(); | ||
} | ||
if (isClientError) throw new (Error as any)(res.statusText, { cause: data }); | ||
return transformOutput(data, req); | ||
} catch { | ||
throw new Error('Parse error'); | ||
} | ||
}; | ||
|
||
return { | ||
request, | ||
get<T>(path: string, params?: ReqParams) { | ||
const i = transformInput(params); | ||
return request<T>(serializationUrl(path, i), { | ||
method: 'GET', | ||
headers: appendHeaders(i), | ||
}); | ||
}, | ||
del<T>(path: string, params?: ReqParams) { | ||
const i = transformInput(params); | ||
return request<T>(serializationUrl(path, i), { | ||
method: 'DELETE', | ||
headers: appendHeaders(i), | ||
}); | ||
}, | ||
put<T>(path: string, body?: ReqBody) { | ||
const i = transformInput(body); | ||
return request<T>(serializationUrl(path), { | ||
method: 'PUT', | ||
headers: appendHeaders(i), | ||
body: serializationBody(i), | ||
}); | ||
}, | ||
post<T>(path: string, body?: ReqBody) { | ||
const i = transformInput(body); | ||
return request<T>(serializationUrl(path), { | ||
method: 'POST', | ||
headers: appendHeaders(i), | ||
body: serializationBody(i), | ||
}); | ||
}, | ||
}; | ||
} | ||
|
||
const { request, del, get, post, put } = initRequest(); | ||
|
||
export { initRequest, request, del, get, post, put }; |