import { isArray, isEmpty } from "lodash-es"; import ky from "ky"; import type { HttpMethod, InvalidResponse, RawArgType, ValidResponse, recoveryMethod } from "@strpc/def"; export type globalRecoveryFunction = (method: HttpMethod, path: string, args: RawArgType, recovery: recoveryMethod, result: InvalidResponse) => Promise<{ justRetryAPI?: boolean; } | void>; export class APIFailed extends Error { constructor(public readonly reason: string, public readonly detail: unknown, public readonly recovery?: recoveryMethod) { super(reason); } } export async function callClientAPI( method: HttpMethod, apiRoot: string, path: string | string[], args: RawArgType, globalRecovery: globalRecoveryFunction | undefined ): Promise; export async function callClientAPI( method: HttpMethod, apiRoot: string, path: string | string[], args: RawArgType, globalRecovery: globalRecoveryFunction | undefined, returnError: undefined ): Promise; export async function callClientAPI( method: HttpMethod, apiRoot: string, path: string | string[], args: RawArgType, globalRecovery: globalRecoveryFunction | undefined, returnError: false ): Promise; export async function callClientAPI( method: HttpMethod, apiRoot: string, path: string | string[], args: RawArgType, globalRecovery: globalRecoveryFunction | undefined, returnError: true ): Promise; export async function callClientAPI( method: HttpMethod, apiRoot: string, path: string | string[], args: RawArgType, globalRecovery: globalRecoveryFunction | undefined, returnError?: boolean ): Promise { if (isArray(path)) { path = [apiRoot, ...path].join("/"); } else if (path.startsWith("/")) { path = `${apiRoot}${path}`; } else { path = `${apiRoot}/${path}`; } if (args && isEmpty(args)) { args = undefined; } try { let trialRemain = 3; let lastErr: ErrorType | undefined = undefined; // eslint-disable-next-line no-constant-condition while(trialRemain > 0){ const result = (await (() => { if (method == "get") { //TODO: args가 복합 object일 경우의 처리 return ky(path, { searchParams: args === undefined ? undefined : { ...(args as Record), }, method, headers: { "content-type": "application/json", }, timeout: 30000, retry: 0, }); } return ky(path, { method, json: args, headers: { "content-type": "application/json", }, timeout: 30000, retry: 0, }); })().json()) as ErrorType | ResultType; if (!result.result) { lastErr = result; if(result.recovery && globalRecovery){ const recoveryResult = await globalRecovery(method, path, args, result.recovery, result); if(recoveryResult !== undefined && recoveryResult.justRetryAPI){ trialRemain--; continue; } } if (returnError) { return result; } throw new APIFailed( result.reason, result.detail, result.recovery ); } return result; } if(lastErr === undefined){ throw new Error('lastErr is undefined'); } if (returnError) { return lastErr; } throw lastErr.reason; } catch(err) { if(!returnError){ throw err; } let infoText = '' if(err instanceof APIFailed){ infoText = err.reason; } else if(err instanceof Error){ infoText = err.toString(); } else if(typeof err == 'string'){ infoText = err; } else{ infoText = JSON.stringify(err); } return { result: false, reason: `failed to fetch(${path}): ${infoText}`, detail: err, } as ErrorType; } }