import { createAPIError } from '@/common/report/errors'; import { createFetch } from '../network/create-fetch'; import { createSyncAPI, registerCanIUse, t } from './base'; import { dirtyURL, normalizeHeader, normalizeMethod, normalizeTimeout } from '../network/utils'; import { BaseError, createCommands, Deferred, wrapUserFunction } from '@jolibox/common'; import { AnyFunction, ResponseType } from '@jolibox/types'; import { reportError } from '@/common/report/errors/report'; import { FetchOptions } from '../network/types'; import { context } from '@/common/context'; const LIMIT = 15; const publicFetch = createFetch('createRequestTaskSync', 'operateRequestTaskSync', { type: 'public', baseUrl: context.baseApiHost }); /** current active count */ let activeCount = 0; const deferQueue: AnyFunction[] = []; const safeCallbackWrapper = wrapUserFunction(reportError as (err: Error | BaseError) => void); const remove = (array: T[], predicate: (item: T) => boolean) => { const removedElements = array.filter(predicate); const remainingElements = array.filter((item) => !predicate(item)); array.length = 0; array.push(...remainingElements); return removedElements; }; const request = (_params: unknown) => { const deferred = new Deferred<{ code: ResponseType; data?: string | JSON | ArrayBuffer | undefined; message: string; }>(); let teardown = () => { // noop }; const _request = createSyncAPI('request', { paramsSchema: t.tuple( t.object({ url: t.string(), method: t.string(), headers: t.object().optional(), data: t.or(t.object(), t.array()).optional(), query: t.object().optional(), dataType: t.string().default('json'), responseType: t.string().default('text'), enableCache: t.boolean().default(false), appendHostCookie: t.boolean().default(true), timeout: t.number().default(30000), success: t.function(), fail: t.function() }) ), implement(params) { const action = async () => { const failCallback = safeCallbackWrapper(params.fail); try { activeCount += 1; // 在 action 执行前, 如果用户就调用了 abort 的话就会命中该逻辑 if (deferred.state !== 'pending') { return; } const { data, dataType, responseType, enableCache, appendHostCookie } = params; const header = normalizeHeader(params.headers ?? {}); const method = normalizeMethod(params.method); const timeout = normalizeTimeout(params.timeout); const url = dirtyURL(params.url, method, data); const successCallback = safeCallbackWrapper(params.success); const _params = Object.assign({}, params, { method, header: appendHostCookie ? undefined : header, data: method === 'GET' || method === 'HEAD' ? undefined : data, // GET HEAD 请求不需要 data 参数 enableCache, query: params.query ?? ({} as Record), dataType, responseType, appendHostCookie }) as FetchOptions; if (timeout) _params['timeout'] = timeout; const task = publicFetch< string | ArrayBuffer | JSON | { status: number; data: string | ArrayBuffer | JSON } >(url, _params); { const { response } = await task; const { code, data, message } = response; if (data && typeof data === 'object' && 'status' in data && data.status !== 200) { throw createAPIError({ code: data.status || -1, msg: `request:fail status ${data.status}` }); } deferred.resolve({ code: (code ?? 'SUCCESS') as ResponseType, data: data as string | ArrayBuffer | JSON, message: message ?? 'request:ok' }); successCallback({ code: 'SUCCESS', message: 'request:ok', data }); } } catch (err) { deferred.reject(err as Error); failCallback({ code: 'FAILURE', message: 'httpRequst: failed' }); } finally { activeCount -= 1; deferQueue.shift()?.(); } }; if (activeCount >= LIMIT) { teardown = () => { remove(deferQueue, (i) => i === action); }; deferQueue.push(action); } else { void action(); } return deferred.promise; } }); _request(_params as Parameters[0]); return { /** * 1. requesting... * 2. not in deferred */ abort() { deferred.reject(createAPIError({ code: -1, msg: 'request:fail abort' })); teardown(); } }; }; const commands = createCommands(); commands.registerCommand('HttpSDK.request', request); registerCanIUse('request', { version: '1.0.0' });