import { stringify } from "query-string" ; import { typesafePick } from "../../lang" ; export class BadResponseError extends Error { constructor ( public response ) { super ( ) ; } } export class TimeoutError extends Error { } export class NetworkError extends Error { } export const SessionName = { DownloadStation : "DownloadStation" as const , FileStation : "FileStation" as const , } ; export type SessionName = keyof typeof SessionName ; export interface FormFile { content : Blob ; filename : string ; } export function isFormFile ( f ? ) { return f && ( f as FormFile ) . content != null && ( f as FormFile ) . filename != null ; } export interface ApiGroupMeta { apiGroup : string ; apiSubgroup ? : string ; } export interface ResponseMeta extends ApiGroupMeta { method : string ; version : number ; } export interface RestApiSuccessResponse < S > { success : true ; data : S ; meta : ResponseMeta ; } export interface RestApiFailureResponse { success : false ; meta : ResponseMeta ; error : { code : number ; errors ? : any [ ] ; } ; } export type RestApiResponse < S > = RestApiSuccessResponse < S > | RestApiFailureResponse ; export interface BaseRequest { timeout ? : number ; } export interface ApiRequest { api : string ; version : number ; method : string ; meta : ApiGroupMeta ; sid ? : string ; timeout ? : number ; [ key ] : string | number | boolean | FormFile | ApiGroupMeta | undefined ; } const DEFAULT_TIMEOUT = 60000 ; async function fetchWithErrorHandling ( url , init , timeout , meta ) { const abortController = new AbortController ( ) ; const timeoutTimer = setTimeout ( ( ) => { abortController . abort ( ) ; } , timeout ?? DEFAULT_TIMEOUT ) ; try { const response = await fetch ( url , { ... init , credentials : "omit" , signal : abortController . signal , } ) ; if ( ! response . ok ) { throw new BadResponseError ( response ) ; } else { return { ... ( await response . json ( ) ) , meta , } ; } } catch ( e ) { if ( e instanceof DOMException && e . name === "AbortError" ) { throw new TimeoutError ( ) ; } else if ( e instanceof Error && /networkerror/i . test ( e ?. message ) ) { throw new NetworkError ( ) ; } else { throw e ; } } finally { clearTimeout ( timeoutTimer ) ; } } export async function get < O extends object > ( baseUrl , cgi , request ) { const url = `${ baseUrl }/webapi/${ cgi }.cgi?${ stringify ( { ... request , _sid : request . sid , timeout : undefined , meta : undefined , } ) }` ; return fetchWithErrorHandling ( url , { method : "GET" } , request . timeout , { ... request . meta , ... typesafePick ( request , "method" , "version" ) , } ) as Promise < RestApiResponse < O > > ; } export async function post < O extends object > ( baseUrl , cgi , request ) { const formData = new FormData ( ) ; Object . keys ( request ) . forEach ( ( k ) => { const v = request [ k ] ; if ( k !== "timeout" && k !== "meta" && v !== undefined && ! isFormFile ( v ) ) { formData . append ( k , String ( v ) ) ; } } ) ; if ( request . sid ) { formData . append ( "_sid" , request . sid ) ; } Object . keys ( request ) . forEach ( ( k ) => { const v = request [ k ] ; if ( k !== "timeout" && k !== "meta" && v !== undefined && isFormFile ( v ) ) { formData . append ( k , v . content , v . filename ) ; } } ) ; const url = `${ baseUrl }/webapi/${ cgi }.cgi?${ stringify ( { _sid : request . sid } ) }` ; return fetchWithErrorHandling ( url , { method : "POST" , body : formData } , request . timeout , { ... request . meta , ... typesafePick ( request , "method" , "version" ) , } ) as Promise < RestApiResponse < O > > ; } export class ApiBuilder { constructor ( private cgiName , private apiName , private meta ) { } makeGet < I extends BaseRequest , O > ( methodName , preprocess ? , postprocess ? ) ; makeGet < I extends BaseRequest , O > ( methodName , preprocess , postprocess , optional ) ; makeGet ( methodName , preprocess ? , postprocess ? , _optional ? ) { return this . makeApiRequest ( get , methodName , preprocess , postprocess ) ; } makePost < I extends BaseRequest , O > ( methodName , preprocess ? , postprocess ? ) ; makePost < I extends BaseRequest , O > ( methodName , preprocess , postprocess , optional ) ; makePost ( methodName , preprocess ? , postprocess ? , _optional ? ) { return this . makeApiRequest ( post , methodName , preprocess , postprocess ) ; } private makeApiRequest ( method , methodName , preprocess ? , postprocess ? ) { preprocess = preprocess || ( ( o ) => o ) ; postprocess = postprocess || ( ( r ) => r ) ; return async ( baseUrl , sid , options ? ) => { const response = await method ( baseUrl , this . cgiName , { ... preprocess ! ( options || { } ) , api : this . apiName , version : 1 , method : methodName , sid , meta : this . meta , } ) ; if ( response . success ) { return { ... response , data : postprocess ! ( response . data ) } ; } else { return response ; } } ; } }