@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/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()