import { isString, logger, Deferred, isObject, hostEmitter, isUndefined, uuidv4, getApiHost } from '@jolibox/common'; import { FetchResponse, FetchOptions } from './types'; import { invokeNative, onNative } from '@jolibox/native-bridge'; import { AnyFunction } from '@jolibox/types'; import { reportError } from '@/common/report/errors/report'; import { createAPIError } from '@/common/report/errors'; import { context } from '@/common/context'; type Fetch = ( url: string, innerOptions?: FetchOptions & { baseUrl?: string; } ) => Promise<{ url: string; timeout?: number; method: string; response: FetchResponse; }>; type PublicFetch = ( url: string, options?: FetchOptions ) => Promise<{ url: string; timeout?: number; method: string; response: FetchResponse; }>; const processUrl = (url: string, options?: FetchOptions & { baseUrl?: string }): string => { if (/^https?:\/\//.test(url)) { return url; } const query = options?.query; const searchParams = new URLSearchParams(query); const search = searchParams.toString(); return options?.baseUrl ? `${options.baseUrl}${url}${search ? `?${search}` : ''}` : `${url}${search ? `?${search}` : ''}`; }; export function createFetch( createMethod: 'createRequestTaskSync', operateMethod: 'operateRequestTaskSync', options?: { baseUrl?: string; defaultHeaders?: Record; type: 'public'; } ): PublicFetch; export function createFetch( createMethod: 'createRequestTaskSync', operateMethod: 'operateRequestTaskSync', options?: { baseUrl?: string; type: 'inner'; defaultHeaders?: Record; } ): Fetch; export function createFetch( createMethod: 'createRequestTaskSync', operateMethod: 'operateRequestTaskSync', options?: { baseUrl?: string; type: 'inner' | 'public'; defaultHeaders?: Record; } ) { const promiseMap = new Map(); const baseUrl = options?.baseUrl ?? getApiHost(context.testMode); const type = options?.type ?? 'public'; const defaultHeaders = options?.defaultHeaders ?? {}; onNative('onRequestTaskStateChange', (res) => { if (typeof res.requestTaskId !== 'string') return; const task = promiseMap.get(res.requestTaskId); if (!task) return; if (res.state === 'success') { const { requestTaskId, state, ...rest } = res; if (res.statusCode === 200) { task.resolve(rest); } else { task.reject(rest); } promiseMap.delete(requestTaskId); } else if (res.state === 'fail') { const { requestTaskId, state, ...rest } = res; task.reject(rest); promiseMap.delete(requestTaskId); } else { logger.warn(`onRequestTaskStateChange unknown event`, res); } }); hostEmitter.on('onLoginComplete', ({ isLogin, token }) => { if (type === 'public') { return; } if (isLogin && token) { defaultHeaders['X-JOLI-TOKEN'] = token; } }); const fetch: Fetch = async (originUrl, options = {}) => { const startTime = Date.now(); const url = processUrl(originUrl, { ...options, baseUrl }); const { method = 'GET', responseType = 'text', dataType = 'json', timeout, enableCache = false, appendHostCookie = true } = options; const header = Object.assign({ 'content-type': 'application/json' }, defaultHeaders, options.header); // modify params const params = { url, method, header, enableCache, responseType, appendHostCookie } as any; if (method == 'POST' || method == 'PUT') { const data = normalizeData(options.data, header['content-type']); params['data'] = data; } if (timeout) { Object.assign(params, { timeout }); } const { data: { requestTaskId } } = invokeNative(createMethod, params); if (typeof requestTaskId !== 'string') { throw createAPIError({ code: -1, msg: 'requestTaskId is not a string' }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any const { resolve, reject, promise } = new Deferred>(); promiseMap.set(requestTaskId, { resolve, reject }); try { const res = await promise; let { data } = res; if (dataType === 'json' && typeof data === 'string') { data = parseJSON(data); } return { url, timeout, method, response: { ...res, data } }; } catch (err: unknown) { const error = err as { errMsg?: string; errNo?: number; errorType?: string; errorCode?: number; prefetchDetail?: number; isPrefetch?: boolean; }; if (isObject(error)) { const { errMsg, prefetchDetail, isPrefetch, errNo } = error; if (isString(errMsg)) { reportError(createAPIError({ code: -1, msg: errMsg }, { errNo: errNo as number, errMsg })); } } throw err; } }; return fetch; } export function parseJSON(data: string) { try { // // 之所以要 trim, 是因为有些开发者不是按照标准的 application/json 返回的, 而是使用 text/html 返回 // 而 text/html 返回的时候前后有可能出现  字符, 导致 JSON.parse 失败 return JSON.parse(data.trim()); } catch { return data; } } export function normalizeData(data: unknown, contentType = 'application/json') { const type = contentType.toLowerCase(); if (data === undefined) { return ''; } if (typeof data === 'string' || data instanceof ArrayBuffer) { return data; } if (type.includes('application/x-www-form-urlencoded')) { return JSON.stringify(data as JSON); } if (type.includes('application/json')) { return JSON.stringify(data); } if (typeof data === 'object') { return JSON.stringify(data); } return String(data); } const normalizeRequestBody = ( data: unknown, contentType = 'application/json' ): { body: string; dataType: 'json' | 'text'; } => { if (isUndefined(data) || data instanceof ArrayBuffer) { return { body: '{}', dataType: 'json' }; } if (typeof data === 'string') { return { body: data, dataType: 'text' }; } const type = contentType.toLowerCase(); if ( (type.includes('application/x-www-form-urlencoded') || type.includes('application/json')) && typeof data === 'object' ) { return { body: JSON.stringify(data), dataType: 'json' }; } return { body: String(data), dataType: 'text' }; }; export const sendRequestBodyToNative = async (data: unknown, contentType: string) => { const { body, dataType } = normalizeRequestBody(data, contentType); const requestId = uuidv4(); await invokeNative('sendRequestBodyAsync', { requestId, body, dataType }); };