@follow-app/client-sdk
Version:
TypeScript client SDK for Follow RSS Server API
260 lines (228 loc) • 6.31 kB
text/typescript
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
}
},
}