UNPKG

@follow-app/client-sdk

Version:

TypeScript client SDK for Follow RSS Server API

260 lines (228 loc) 6.31 kB
import type { RequestOptions } from "../types" /** * Base context object shared by all interceptors */ interface BaseInterceptorContext { url: string options: RequestOptions } /** * Context object passed to request interceptors */ export interface RequestInterceptorContext extends BaseInterceptorContext {} /** * Context object passed to response interceptors */ export interface ResponseInterceptorContext extends BaseInterceptorContext { response: Response } /** * Context object passed to error interceptors */ export interface ErrorInterceptorContext extends BaseInterceptorContext { response: Response | null error: Error } /** * Request interceptor function type */ export type RequestInterceptor = ( ctx: RequestInterceptorContext, ) => | Promise<{ url: string, options: RequestOptions }> | { url: string, options: RequestOptions } /** * Response interceptor function type */ export type ResponseInterceptor = ( ctx: ResponseInterceptorContext, ) => Promise<Response> | Response /** * Error interceptor function type */ export type ErrorInterceptor = ( ctx: ErrorInterceptorContext, ) => Promise<Error | void> | Error | void /** * Interceptor manager for handling request/response middleware */ export class InterceptorManager { private requestInterceptors: RequestInterceptor[] = [] private responseInterceptors: ResponseInterceptor[] = [] private errorInterceptors: ErrorInterceptor[] = [] /** * Generic method to add an interceptor to any array and return a cleanup function */ private addInterceptor<T>(interceptor: T, interceptors: T[]): () => void { interceptors.push(interceptor) return () => { const index = interceptors.indexOf(interceptor) if (index !== -1) { interceptors.splice(index, 1) } } } /** * Add a request interceptor */ addRequestInterceptor(interceptor: RequestInterceptor): () => void { return this.addInterceptor(interceptor.bind(null), this.requestInterceptors) } /** * Add a response interceptor */ addResponseInterceptor(interceptor: ResponseInterceptor): () => void { return this.addInterceptor(interceptor.bind(null), this.responseInterceptors) } /** * Add an error interceptor */ addErrorInterceptor(interceptor: ErrorInterceptor): () => void { return this.addInterceptor(interceptor.bind(null), this.errorInterceptors) } /** * Process request through all request interceptors */ async processRequest( url: string, options: RequestOptions, ): Promise<{ url: string, options: RequestOptions }> { let currentUrl = url let currentOptions = options for (const interceptor of this.requestInterceptors) { const ctx: RequestInterceptorContext = { url: currentUrl, options: currentOptions, } const result = (await interceptor(ctx)) || ctx currentUrl = result.url currentOptions = result.options } return { url: currentUrl, options: currentOptions } } /** * Process response through all response interceptors */ async processResponse( response: Response, url: string, options: RequestOptions, ): Promise<Response> { let currentResponse = response for (const interceptor of this.responseInterceptors) { const ctx: ResponseInterceptorContext = { url, options, response: currentResponse, } const returnedResponse = await interceptor(ctx) if (returnedResponse instanceof Response) { currentResponse = returnedResponse } // If interceptor returns undefined, it means the response should not be modified } return currentResponse } /** * Process error through all error interceptors */ async processError( error: Error, response: Response | null, url: string, options: RequestOptions, ): Promise<Error | void> { let currentError: Error | void = error for (const interceptor of this.errorInterceptors) { const ctx: ErrorInterceptorContext = { url, options, response, error: currentError || error, } const result = await interceptor(ctx) if (result !== undefined) { currentError = result } else { // If interceptor returns undefined, it means the error should be handled/suppressed currentError = undefined break } } return currentError } /** * Clear all interceptors */ clear(): void { this.requestInterceptors = [] this.responseInterceptors = [] this.errorInterceptors = [] } } /** * Common interceptors for Follow API */ export const commonInterceptors = { /** * Add authentication token to requests */ addAuthToken: (token: string): RequestInterceptor => { return (ctx) => { return { url: ctx.url, options: { ...ctx.options, headers: { ...ctx.options.headers, Authorization: `Bearer ${token}`, }, }, } } }, /** * Log all requests */ logRequests: (logger: { log: (message: string) => void }): RequestInterceptor => { return (ctx) => { logger.log(`Request: ${ctx.options.method || "GET"} ${ctx.url}`) return { url: ctx.url, options: ctx.options } } }, /** * Log all responses */ logResponses: (logger: { log: (message: string) => void }): ResponseInterceptor => { return (ctx) => { logger.log( `Response: ${ctx.response.status} ${ctx.options.method || "GET"} ${ctx.url}`, ) return ctx.response } }, /** * Retry failed requests */ retryOnError: (maxRetries = 3, delay = 1000): ErrorInterceptor => { const retryCount = new WeakMap<Error, number>() return async (ctx) => { const currentRetries = retryCount.get(ctx.error) || 0 if (currentRetries < maxRetries) { retryCount.set(ctx.error, currentRetries + 1) // Wait before retry await new Promise((resolve) => setTimeout(resolve, delay * (currentRetries + 1)), ) // Return undefined to indicate retry should happen return } // Max retries exceeded, return the error return ctx.error } }, }