@zairakai/js-http-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/chunk-SSH6WKEL.js +74 -0
- package/dist/index.cjs +244 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +139 -0
- package/dist/interceptors-Pv-t9Hbf.d.cts +93 -0
- package/dist/interceptors-Pv-t9Hbf.d.ts +93 -0
- package/dist/interceptors.cjs +113 -0
- package/dist/interceptors.d.cts +2 -0
- package/dist/interceptors.d.ts +2 -0
- package/dist/interceptors.js +16 -0
- package/package.json +107 -0
- package/src/client.ts +207 -0
- package/src/index.ts +31 -0
- package/src/interceptors.ts +167 -0
- package/src/request-tracker.ts +56 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client factory with configurable options
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
|
6
|
+
import {
|
|
7
|
+
Logger,
|
|
8
|
+
ShouldRetryFunction,
|
|
9
|
+
TokenSource,
|
|
10
|
+
createAuthInterceptor,
|
|
11
|
+
createCSRFInterceptor,
|
|
12
|
+
createErrorLoggerInterceptor,
|
|
13
|
+
createRetryInterceptor,
|
|
14
|
+
createTimeoutInterceptor,
|
|
15
|
+
createTrackingInterceptors,
|
|
16
|
+
} from './interceptors.js'
|
|
17
|
+
import { RequestTracker, createRequestTracker } from './request-tracker.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* HTTP client configuration options
|
|
21
|
+
*/
|
|
22
|
+
export interface HttpClientOptions {
|
|
23
|
+
/** Base URL for requests */
|
|
24
|
+
baseURL?: string
|
|
25
|
+
/** Default headers */
|
|
26
|
+
headers?: Record<string, string>
|
|
27
|
+
/** Include credentials */
|
|
28
|
+
withCredentials?: boolean
|
|
29
|
+
/** Request timeout in milliseconds */
|
|
30
|
+
timeout?: number
|
|
31
|
+
/** Enable request tracking */
|
|
32
|
+
trackRequests?: boolean
|
|
33
|
+
/** CSRF token or getter function */
|
|
34
|
+
csrfToken?: TokenSource
|
|
35
|
+
/** Auth token or getter function */
|
|
36
|
+
authToken?: TokenSource
|
|
37
|
+
/** Auth token type (Bearer, Token, etc.) */
|
|
38
|
+
authType?: string
|
|
39
|
+
/** Enable error logging */
|
|
40
|
+
enableErrorLogging?: boolean
|
|
41
|
+
/** Custom logger function */
|
|
42
|
+
logger?: Logger
|
|
43
|
+
/** Number of retries for failed requests */
|
|
44
|
+
retries?: number
|
|
45
|
+
/** Delay between retries in milliseconds */
|
|
46
|
+
retryDelay?: number
|
|
47
|
+
/** Custom retry logic */
|
|
48
|
+
shouldRetry?: ShouldRetryFunction
|
|
49
|
+
/** Enable Laravel-specific features */
|
|
50
|
+
laravel?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* HTTP client instance with convenience methods
|
|
55
|
+
*/
|
|
56
|
+
export interface HttpClient {
|
|
57
|
+
/** Axios instance */
|
|
58
|
+
readonly client: AxiosInstance
|
|
59
|
+
/** Request tracker instance (null if tracking disabled) */
|
|
60
|
+
readonly tracker: RequestTracker | null
|
|
61
|
+
/** Whether there are active requests */
|
|
62
|
+
readonly isLoading: boolean
|
|
63
|
+
/** Current number of active requests */
|
|
64
|
+
readonly requestCount: number
|
|
65
|
+
|
|
66
|
+
// Convenience HTTP methods
|
|
67
|
+
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
|
|
68
|
+
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
|
|
69
|
+
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
|
|
70
|
+
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
|
|
71
|
+
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Default configuration for HTTP clients
|
|
76
|
+
*/
|
|
77
|
+
const DEFAULT_CONFIG: Required<Pick<HttpClientOptions, 'baseURL' | 'timeout' | 'withCredentials' | 'headers'>> = {
|
|
78
|
+
baseURL: '',
|
|
79
|
+
timeout: 10000,
|
|
80
|
+
withCredentials: false,
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
Accept: 'application/json',
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a configured HTTP client with interceptors
|
|
89
|
+
* @param options - Configuration options
|
|
90
|
+
* @returns Configured axios instance with tracker
|
|
91
|
+
*/
|
|
92
|
+
export const createHttpClient = (options: HttpClientOptions = {}): HttpClient => {
|
|
93
|
+
const config = { ...DEFAULT_CONFIG, ...options }
|
|
94
|
+
|
|
95
|
+
// Create axios instance
|
|
96
|
+
const client = axios.create({
|
|
97
|
+
baseURL: config.baseURL,
|
|
98
|
+
timeout: config.timeout,
|
|
99
|
+
withCredentials: config.withCredentials,
|
|
100
|
+
headers: { ...DEFAULT_CONFIG.headers, ...config.headers },
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Create request tracker if enabled
|
|
104
|
+
const tracker = false !== config.trackRequests ? createRequestTracker() : null
|
|
105
|
+
|
|
106
|
+
// Add request tracking interceptors
|
|
107
|
+
if (tracker) {
|
|
108
|
+
const trackingInterceptors = createTrackingInterceptors(tracker)
|
|
109
|
+
client.interceptors.request.use(trackingInterceptors.request, trackingInterceptors.requestError)
|
|
110
|
+
client.interceptors.response.use(trackingInterceptors.response, trackingInterceptors.responseError)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add CSRF interceptor
|
|
114
|
+
if (config.csrfToken) {
|
|
115
|
+
client.interceptors.request.use(createCSRFInterceptor(config.csrfToken))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add auth interceptor
|
|
119
|
+
if (config.authToken) {
|
|
120
|
+
client.interceptors.request.use(createAuthInterceptor(config.authToken, config.authType))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add timeout interceptor if different from default
|
|
124
|
+
if (config.timeout !== DEFAULT_CONFIG.timeout) {
|
|
125
|
+
client.interceptors.request.use(createTimeoutInterceptor(config.timeout))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Add error logging interceptor
|
|
129
|
+
if (false !== config.enableErrorLogging) {
|
|
130
|
+
client.interceptors.response.use((response) => response, createErrorLoggerInterceptor(config.logger))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add retry interceptor
|
|
134
|
+
if (config.retries && 0 < config.retries) {
|
|
135
|
+
// Add the client instance to configs for retry handling
|
|
136
|
+
client.interceptors.request.use((reqConfig) => {
|
|
137
|
+
;(reqConfig as any)._axios = client
|
|
138
|
+
return reqConfig
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
client.interceptors.response.use(
|
|
142
|
+
(response) => response,
|
|
143
|
+
createRetryInterceptor(config.retries, config.retryDelay, config.shouldRetry)
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add Laravel-style headers if not disabled
|
|
148
|
+
if (false !== config.laravel) {
|
|
149
|
+
client.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
client,
|
|
154
|
+
tracker,
|
|
155
|
+
|
|
156
|
+
// Convenience methods
|
|
157
|
+
get: <T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
|
|
158
|
+
client.get<T>(url, config),
|
|
159
|
+
post: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
|
|
160
|
+
client.post<T>(url, data, config),
|
|
161
|
+
put: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
|
|
162
|
+
client.put<T>(url, data, config),
|
|
163
|
+
patch: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
|
|
164
|
+
client.patch<T>(url, data, config),
|
|
165
|
+
delete: <T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> =>
|
|
166
|
+
client.delete<T>(url, config),
|
|
167
|
+
|
|
168
|
+
// Request status
|
|
169
|
+
get isLoading(): boolean {
|
|
170
|
+
/* c8 ignore next */
|
|
171
|
+
return tracker ? tracker.isActive : false
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
get requestCount(): number {
|
|
175
|
+
/* c8 ignore next */
|
|
176
|
+
return tracker ? tracker.count : 0
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Creates a Laravel-compatible HTTP client
|
|
183
|
+
* @param options - Configuration options
|
|
184
|
+
* @returns Configured HTTP client
|
|
185
|
+
*/
|
|
186
|
+
export const createLaravelClient = (options: HttpClientOptions = {}): HttpClient => {
|
|
187
|
+
return createHttpClient({
|
|
188
|
+
withCredentials: true,
|
|
189
|
+
headers: {
|
|
190
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
191
|
+
},
|
|
192
|
+
...options,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Creates an API client (typically for external APIs)
|
|
198
|
+
* @param options - Configuration options
|
|
199
|
+
* @returns Configured HTTP client
|
|
200
|
+
*/
|
|
201
|
+
export const createApiClient = (options: HttpClientOptions = {}): HttpClient => {
|
|
202
|
+
return createHttpClient({
|
|
203
|
+
withCredentials: false,
|
|
204
|
+
laravel: false,
|
|
205
|
+
...options,
|
|
206
|
+
})
|
|
207
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zairakai/npm-http-client
|
|
3
|
+
* Configurable HTTP client built on axios with request tracking and Laravel support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
createApiClient,
|
|
8
|
+
createHttpClient,
|
|
9
|
+
createLaravelClient,
|
|
10
|
+
type HttpClient,
|
|
11
|
+
type HttpClientOptions,
|
|
12
|
+
} from './client.js'
|
|
13
|
+
|
|
14
|
+
export { createRequestTracker, globalRequestTracker, type RequestTracker } from './request-tracker.js'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
createAuthInterceptor,
|
|
18
|
+
createCSRFInterceptor,
|
|
19
|
+
createErrorLoggerInterceptor,
|
|
20
|
+
createRetryInterceptor,
|
|
21
|
+
createTimeoutInterceptor,
|
|
22
|
+
createTrackingInterceptors,
|
|
23
|
+
type Logger,
|
|
24
|
+
type ShouldRetryFunction,
|
|
25
|
+
type TokenSource,
|
|
26
|
+
type TrackingInterceptors,
|
|
27
|
+
} from './interceptors.js'
|
|
28
|
+
|
|
29
|
+
// Re-export axios for convenience
|
|
30
|
+
export { default as axios } from 'axios'
|
|
31
|
+
export type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, Method as HttpMethod } from 'axios'
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common interceptors for HTTP clients
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
|
6
|
+
import { RequestTracker } from './request-tracker.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Token source type - either a string or a function that returns a string
|
|
10
|
+
*/
|
|
11
|
+
export type TokenSource = string | (() => string)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Logger function type
|
|
15
|
+
*/
|
|
16
|
+
export type Logger = (message: string, data?: unknown) => void
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retry condition function type
|
|
20
|
+
*/
|
|
21
|
+
export type ShouldRetryFunction = (error: AxiosError) => boolean
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tracking interceptors interface
|
|
25
|
+
*/
|
|
26
|
+
export interface TrackingInterceptors {
|
|
27
|
+
request: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
|
|
28
|
+
requestError: (error: Error) => Promise<never>
|
|
29
|
+
response: (response: AxiosResponse) => AxiosResponse
|
|
30
|
+
responseError: (error: Error) => Promise<never>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates request tracking interceptors
|
|
35
|
+
* @param tracker - Request tracker instance
|
|
36
|
+
* @returns Request and response interceptors
|
|
37
|
+
*/
|
|
38
|
+
export const createTrackingInterceptors = (tracker: RequestTracker): TrackingInterceptors => ({
|
|
39
|
+
request: (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
|
40
|
+
tracker.increment()
|
|
41
|
+
return config
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
requestError: (error: Error): Promise<never> => {
|
|
45
|
+
return Promise.reject(error)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
response: (response: AxiosResponse): AxiosResponse => {
|
|
49
|
+
tracker.decrement()
|
|
50
|
+
return response
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
responseError: (error: Error): Promise<never> => {
|
|
54
|
+
tracker.decrement()
|
|
55
|
+
return Promise.reject(error)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates Laravel CSRF token interceptor
|
|
61
|
+
* @param tokenSource - CSRF token or function that returns token
|
|
62
|
+
* @returns Request interceptor
|
|
63
|
+
*/
|
|
64
|
+
export const createCSRFInterceptor =
|
|
65
|
+
(tokenSource: TokenSource) =>
|
|
66
|
+
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
|
67
|
+
const token = 'function' === typeof tokenSource ? tokenSource() : tokenSource
|
|
68
|
+
|
|
69
|
+
if (token) {
|
|
70
|
+
config.headers = config.headers ?? {}
|
|
71
|
+
config.headers['X-CSRF-TOKEN'] = token
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return config
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates authentication interceptor
|
|
79
|
+
* @param tokenSource - Auth token or function that returns token
|
|
80
|
+
* @param type - Token type ('Bearer', 'Token', etc.)
|
|
81
|
+
* @returns Request interceptor
|
|
82
|
+
*/
|
|
83
|
+
export const createAuthInterceptor =
|
|
84
|
+
(tokenSource: TokenSource, type: string = 'Bearer') =>
|
|
85
|
+
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
|
86
|
+
const token = 'function' === typeof tokenSource ? tokenSource() : tokenSource
|
|
87
|
+
|
|
88
|
+
if (token) {
|
|
89
|
+
config.headers = config.headers ?? {}
|
|
90
|
+
config.headers['Authorization'] = `${type} ${token}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return config
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates error logging interceptor
|
|
98
|
+
* @param logger - Logging function
|
|
99
|
+
* @returns Response error interceptor
|
|
100
|
+
*/
|
|
101
|
+
export const createErrorLoggerInterceptor =
|
|
102
|
+
(logger: Logger = console.error) =>
|
|
103
|
+
(error: AxiosError): Promise<never> => {
|
|
104
|
+
const logData = {
|
|
105
|
+
message: error.message,
|
|
106
|
+
status: error.response?.status,
|
|
107
|
+
statusText: error.response?.statusText,
|
|
108
|
+
url: error.config?.url,
|
|
109
|
+
method: error.config?.method?.toUpperCase(),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logger('HTTP Error:', logData)
|
|
113
|
+
return Promise.reject(error)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates timeout interceptor
|
|
118
|
+
* @param timeout - Default timeout in milliseconds
|
|
119
|
+
* @returns Request interceptor
|
|
120
|
+
*/
|
|
121
|
+
export const createTimeoutInterceptor =
|
|
122
|
+
(timeout: number) =>
|
|
123
|
+
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
|
124
|
+
config.timeout = timeout
|
|
125
|
+
return config
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Interface for retry configuration in request config
|
|
130
|
+
*/
|
|
131
|
+
interface RetryConfig extends InternalAxiosRequestConfig {
|
|
132
|
+
__retryCount?: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates retry interceptor
|
|
137
|
+
* @param retries - Number of retries
|
|
138
|
+
* @param delay - Delay between retries in milliseconds
|
|
139
|
+
* @param shouldRetry - Function to determine if request should be retried
|
|
140
|
+
* @returns Response error interceptor
|
|
141
|
+
*/
|
|
142
|
+
export const createRetryInterceptor = (
|
|
143
|
+
retries: number = 3,
|
|
144
|
+
delay: number = 1000,
|
|
145
|
+
shouldRetry: ShouldRetryFunction = () => true
|
|
146
|
+
) => {
|
|
147
|
+
return async (error: AxiosError): Promise<any> => {
|
|
148
|
+
const config = error.config as RetryConfig | undefined
|
|
149
|
+
|
|
150
|
+
if (!config || (config.__retryCount ?? 0) >= retries) {
|
|
151
|
+
return Promise.reject(error)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!shouldRetry(error)) {
|
|
155
|
+
return Promise.reject(error)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
config.__retryCount = (config.__retryCount ?? 0) + 1
|
|
159
|
+
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
161
|
+
|
|
162
|
+
// Try to use the instance from the error, otherwise fall back to axios
|
|
163
|
+
/* c8 ignore next */
|
|
164
|
+
const axiosInstance = (config as any)._axios ?? axios
|
|
165
|
+
return axiosInstance.request(config)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request tracking utility for monitoring ongoing HTTP requests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for request tracker instance
|
|
7
|
+
*/
|
|
8
|
+
export interface RequestTracker {
|
|
9
|
+
/** Current number of active requests */
|
|
10
|
+
readonly count: number
|
|
11
|
+
/** Whether there are any active requests */
|
|
12
|
+
readonly isActive: boolean
|
|
13
|
+
/** Increment the request counter */
|
|
14
|
+
increment(): void
|
|
15
|
+
/** Decrement the request counter */
|
|
16
|
+
decrement(): void
|
|
17
|
+
/** Reset the counter to zero */
|
|
18
|
+
reset(): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a request tracker instance
|
|
23
|
+
* @returns Request tracker with counter and methods
|
|
24
|
+
*/
|
|
25
|
+
export const createRequestTracker = (): RequestTracker => {
|
|
26
|
+
let nbrInternal = 0
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
get count(): number {
|
|
30
|
+
return nbrInternal
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
set count(val: number) {
|
|
34
|
+
nbrInternal = Math.max(0, val)
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
get isActive(): boolean {
|
|
38
|
+
return 0 < nbrInternal
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
increment(): void {
|
|
42
|
+
nbrInternal++
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
decrement(): void {
|
|
46
|
+
nbrInternal = Math.max(0, nbrInternal - 1)
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
reset(): void {
|
|
50
|
+
nbrInternal = 0
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Global shared instance for backward compatibility
|
|
56
|
+
export const globalRequestTracker: RequestTracker = createRequestTracker()
|