import * as E from "fp-ts/Either" import { Service } from "dioc" import { MaybeRef, refWithControl } from "@vueuse/core" import { AxiosRequestConfig, AxiosResponse } from "axios" import type { getI18n } from "~/modules/i18n" import { throwError } from "~/helpers/functional/error" import { Component, Ref, computed, reactive, watch, unref, markRaw } from "vue" /** * Defines the response data from an interceptor request run. */ export type NetworkResponse = AxiosResponse & { config?: { timeData?: { startTime: number endTime: number } } } /** * Defines the errors that can occur during interceptor request run. */ export type InterceptorError = | "cancellation" | { humanMessage: { heading: (t: ReturnType) => string description: (t: ReturnType) => string } error?: unknown component?: Component } /** * Defines the result of an interceptor request run. */ export type RequestRunResult = { /** * Cancels the interceptor request run. */ cancel: () => void /** * Promise that resolves when the interceptor request run is finished. */ response: Promise> } /** * Defines whether an interceptor is selectable or not */ export type InterceptorSelectableStatus = | { type: "selectable" } | { type: "unselectable" reason: | { type: "text" text: (t: ReturnType) => string action?: { text: (t: ReturnType) => string onActionClick: () => void } } | { type: "custom" component: Component props: CustomComponentProps } } /** * An interceptor is an object that defines how to run a Hoppscotch request. */ export type Interceptor = { /** * The ID of the interceptor. This should be unique across all registered interceptors. */ interceptorID: string /** * The function that returns the name of the interceptor. * @param t The i18n function. */ name: (t: ReturnType) => MaybeRef /** * Defines whether the interceptor has support for cookies. * If this field is undefined, it is assumed as not supporting cookies. */ supportsCookies?: boolean /** * Defines what to render in the Interceptor section of the Settings page. * Use this space to define interceptor specific settings. * Not setting this will lead to nothing being rendered about this interceptor in the settings page. */ settingsPageEntry?: { /** * The title of the interceptor entry in the settings page. */ entryTitle: (t: ReturnType) => string /** * The component to render in the settings page. */ component: Component } /** * Defines what to render under the entry for the interceptor in the Interceptor selector. */ selectorSubtitle?: Component /** * Defines whether the interceptor is selectable or not. */ selectable: MaybeRef> /** * Runs the interceptor on the given request. * NOTE: Make sure this function doesn't throw, instead when an error occurs, return a Left Either with the error. * @param request The request to run the interceptor on. */ runRequest: (request: AxiosRequestConfig) => RequestRunResult } /** * This service deals with the registration and execution of * interceptors for request execution. */ export class InterceptorService extends Service { public static readonly ID = "INTERCEPTOR_SERVICE" private interceptors: Map = reactive(new Map()) /** * The ID of the currently selected interceptor. * If `null`, there are no interceptors registered or none can be selected. */ public currentInterceptorID: Ref = refWithControl( null as string | null, { onBeforeChange: (value) => { if (!value) { // Only allow `null` if there are no interceptors return this.availableInterceptors.value.length === 0 } if (value && !this.interceptors.has(value)) { console.warn( "Attempt to set current interceptor ID to unknown ID is ignored" ) return false } return true }, } ) /** * List of interceptors that are registered with the service. */ public availableInterceptors = computed(() => Array.from(this.interceptors.values()) ) /** * Gives an instance to the current interceptor. * NOTE: Do not update from here, this is only for reading. */ public currentInterceptor = computed(() => { if (this.currentInterceptorID.value === null) return null return this.interceptors.get(this.currentInterceptorID.value) }) override onServiceInit() { // If the current interceptor is unselectable, select the first selectable one, else null watch([() => this.interceptors, this.currentInterceptorID], () => { if (!this.currentInterceptorID.value) return const interceptor = this.interceptors.get(this.currentInterceptorID.value) if (!interceptor) { this.currentInterceptorID.value = null return } if (unref(interceptor.selectable).type === "unselectable") { this.currentInterceptorID.value = this.availableInterceptors.value.filter( (interceptor) => unref(interceptor.selectable).type === "selectable" )[0]?.interceptorID ?? null } }) } /** * Register an interceptor with the service. * @param interceptor The interceptor to register */ public registerInterceptor(interceptor: Interceptor) { // markRaw so that interceptor state by itself is not fully marked reactive this.interceptors.set(interceptor.interceptorID, markRaw(interceptor)) if (this.currentInterceptorID.value === null) { this.currentInterceptorID.value = interceptor.interceptorID } } /** * Runs a request through the currently selected interceptor. * @param req The request to run * @throws If no interceptor is selected */ public runRequest(req: AxiosRequestConfig): RequestRunResult { if (!this.currentInterceptorID.value) { throw new Error("No interceptor selected") } const interceptor = this.interceptors.get(this.currentInterceptorID.value) ?? throwError( "Current Interceptor ID is not found in the list of registered interceptors" ) return interceptor.runRequest(req) } }