@lytjs/common-http 6.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/dist/index.cjs ADDED
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var HttpError = class extends Error {
5
+ constructor(message, response) {
6
+ super(message);
7
+ this.name = "HttpError";
8
+ this.response = response;
9
+ this.status = response?.status;
10
+ }
11
+ };
12
+ var CancellationToken = class _CancellationToken {
13
+ constructor() {
14
+ this._controller = new AbortController();
15
+ this._linkedTokens = [];
16
+ }
17
+ get signal() {
18
+ return this._controller.signal;
19
+ }
20
+ cancel() {
21
+ this._controller.abort();
22
+ for (const token of this._linkedTokens) {
23
+ token.cancel();
24
+ }
25
+ }
26
+ get isCancelled() {
27
+ return this._controller.signal.aborted;
28
+ }
29
+ static createLinkedToken(...tokens) {
30
+ const linked = new _CancellationToken();
31
+ for (const token of tokens) {
32
+ token._linkedTokens.push(linked);
33
+ if (token.isCancelled) {
34
+ linked.cancel();
35
+ return linked;
36
+ }
37
+ }
38
+ return linked;
39
+ }
40
+ };
41
+ var HttpClient = class {
42
+ constructor(options) {
43
+ this._options = { ...options };
44
+ this._interceptors = [];
45
+ }
46
+ get(url, options) {
47
+ return this.request("GET", url, options);
48
+ }
49
+ post(url, data, options) {
50
+ return this.request("POST", url, { ...options, body: data });
51
+ }
52
+ put(url, data, options) {
53
+ return this.request("PUT", url, { ...options, body: data });
54
+ }
55
+ delete(url, options) {
56
+ return this.request("DELETE", url, options);
57
+ }
58
+ patch(url, data, options) {
59
+ return this.request("PATCH", url, { ...options, body: data });
60
+ }
61
+ async request(method, url, options) {
62
+ let config = this._buildConfig(method, url, options);
63
+ for (const interceptor of this._interceptors) {
64
+ if (interceptor.request) {
65
+ config = await interceptor.request(config);
66
+ }
67
+ }
68
+ try {
69
+ const response = await this._fetch(config);
70
+ let httpResponse = await this._parseResponse(response, config.responseType);
71
+ for (const interceptor of this._interceptors) {
72
+ if (interceptor.response) {
73
+ httpResponse = await interceptor.response(httpResponse);
74
+ }
75
+ }
76
+ return httpResponse;
77
+ } catch (err) {
78
+ for (const interceptor of this._interceptors) {
79
+ if (interceptor.error) {
80
+ const result = interceptor.error(err instanceof HttpError ? err : new HttpError(String(err)));
81
+ if (result !== void 0) return result;
82
+ }
83
+ }
84
+ throw err;
85
+ }
86
+ }
87
+ use(interceptor) {
88
+ this._interceptors.push(interceptor);
89
+ return () => this.eject(interceptor);
90
+ }
91
+ eject(interceptor) {
92
+ const index = this._interceptors.indexOf(interceptor);
93
+ if (index !== -1) {
94
+ this._interceptors.splice(index, 1);
95
+ }
96
+ }
97
+ _buildConfig(method, url, options) {
98
+ const { baseURL = "", headers: defaultHeaders = {} } = this._options;
99
+ const resolvedURL = this._resolveURL(baseURL, url, options?.params);
100
+ const headers = { ...defaultHeaders, ...options?.headers };
101
+ let body = null;
102
+ if (options && "body" in options && options.body !== void 0) {
103
+ if (typeof options.body === "object" && options.body !== null) {
104
+ body = JSON.stringify(options.body);
105
+ if (!headers["Content-Type"]) {
106
+ headers["Content-Type"] = "application/json";
107
+ }
108
+ } else {
109
+ body = String(options.body);
110
+ }
111
+ }
112
+ const timeout = options?.timeout ?? this._options.timeout;
113
+ return {
114
+ method: method.toUpperCase(),
115
+ url: resolvedURL,
116
+ headers,
117
+ body,
118
+ signal: options?.signal,
119
+ responseType: options?.responseType,
120
+ withCredentials: this._options.withCredentials,
121
+ timeout
122
+ };
123
+ }
124
+ _resolveURL(base, url, params) {
125
+ let resolved = base ? base.replace(/\/+$/, "") + "/" + url.replace(/^\/+/, "") : url;
126
+ if (params) {
127
+ const searchParams = new URLSearchParams(params);
128
+ const qs = searchParams.toString();
129
+ if (qs) {
130
+ resolved += (resolved.includes("?") ? "&" : "?") + qs;
131
+ }
132
+ }
133
+ return resolved;
134
+ }
135
+ async _fetch(config) {
136
+ const { method, url, headers, body, signal, withCredentials, timeout } = config;
137
+ let timeoutId;
138
+ let timeoutController;
139
+ if (timeout && timeout > 0) {
140
+ timeoutController = new AbortController();
141
+ timeoutId = setTimeout(() => timeoutController.abort(), timeout);
142
+ if (signal) {
143
+ if (signal.aborted) {
144
+ clearTimeout(timeoutId);
145
+ throw new HttpError("Request was aborted", void 0);
146
+ }
147
+ signal.addEventListener("abort", () => {
148
+ clearTimeout(timeoutId);
149
+ timeoutController.abort();
150
+ }, { once: true });
151
+ }
152
+ }
153
+ const fetchOptions = {
154
+ method,
155
+ headers,
156
+ body,
157
+ credentials: withCredentials ? "include" : void 0,
158
+ signal: timeoutController ? timeoutController.signal : signal
159
+ };
160
+ try {
161
+ const response = await fetch(url, fetchOptions);
162
+ if (!response.ok) {
163
+ const errorResponse = {
164
+ data: null,
165
+ status: response.status,
166
+ statusText: response.statusText,
167
+ headers: this._parseHeaders(response.headers),
168
+ ok: false
169
+ };
170
+ throw new HttpError(
171
+ `HTTP ${response.status}: ${response.statusText}`,
172
+ errorResponse
173
+ );
174
+ }
175
+ return response;
176
+ } finally {
177
+ if (timeoutId !== void 0) {
178
+ clearTimeout(timeoutId);
179
+ }
180
+ }
181
+ }
182
+ async _parseResponse(response, responseType) {
183
+ let data;
184
+ switch (responseType) {
185
+ case "text":
186
+ data = await response.text();
187
+ break;
188
+ case "blob":
189
+ data = await response.blob();
190
+ break;
191
+ case "arraybuffer":
192
+ data = await response.arrayBuffer();
193
+ break;
194
+ case "json":
195
+ default: {
196
+ const text = await response.text();
197
+ data = text ? JSON.parse(text) : null;
198
+ break;
199
+ }
200
+ }
201
+ return {
202
+ data,
203
+ status: response.status,
204
+ statusText: response.statusText,
205
+ headers: this._parseHeaders(response.headers),
206
+ ok: response.ok
207
+ };
208
+ }
209
+ _parseHeaders(headers) {
210
+ const result = {};
211
+ headers.forEach((value, key) => {
212
+ result[key] = value;
213
+ });
214
+ return result;
215
+ }
216
+ };
217
+ function createHttpClient(options) {
218
+ return new HttpClient(options);
219
+ }
220
+ var http = new HttpClient();
221
+
222
+ exports.CancellationToken = CancellationToken;
223
+ exports.HttpClient = HttpClient;
224
+ exports.HttpError = HttpError;
225
+ exports.createHttpClient = createHttpClient;
226
+ exports.http = http;
227
+ //# sourceMappingURL=index.cjs.map
228
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * @lytjs/common-http\r\n * Lightweight HTTP client based on native fetch API\r\n */\r\n\r\n// --- Types ---\r\n\r\nexport interface HttpClientOptions {\r\n baseURL?: string;\r\n headers?: Record<string, string>;\r\n timeout?: number;\r\n withCredentials?: boolean;\r\n}\r\n\r\nexport interface RequestOptions {\r\n headers?: Record<string, string>;\r\n params?: Record<string, string>;\r\n timeout?: number;\r\n signal?: AbortSignal;\r\n responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';\r\n}\r\n\r\nexport interface HttpResponse<T> {\r\n data: T;\r\n status: number;\r\n statusText: string;\r\n headers: Record<string, string>;\r\n ok: boolean;\r\n}\r\n\r\nexport interface InternalRequestConfig {\r\n method: string;\r\n url: string;\r\n headers: Record<string, string>;\r\n body?: BodyInit | null;\r\n signal?: AbortSignal;\r\n responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';\r\n withCredentials?: boolean;\r\n timeout?: number;\r\n}\r\n\r\nexport interface Interceptor {\r\n request?(config: InternalRequestConfig): InternalRequestConfig | Promise<InternalRequestConfig>;\r\n response?(response: HttpResponse<unknown>): HttpResponse<unknown> | Promise<HttpResponse<unknown>>;\r\n error?(error: HttpError): unknown;\r\n}\r\n\r\n// --- HttpError ---\r\n\r\nexport class HttpError extends Error {\r\n readonly response?: HttpResponse<unknown>;\r\n readonly status?: number;\r\n\r\n constructor(message: string, response?: HttpResponse<unknown>) {\r\n super(message);\r\n this.name = 'HttpError';\r\n this.response = response;\r\n this.status = response?.status;\r\n }\r\n}\r\n\r\n// --- CancellationToken ---\r\n\r\nexport class CancellationToken {\r\n private readonly _controller: AbortController;\r\n private readonly _linkedTokens: CancellationToken[];\r\n\r\n constructor() {\r\n this._controller = new AbortController();\r\n this._linkedTokens = [];\r\n }\r\n\r\n get signal(): AbortSignal {\r\n return this._controller.signal;\r\n }\r\n\r\n cancel(): void {\r\n this._controller.abort();\r\n for (const token of this._linkedTokens) {\r\n token.cancel();\r\n }\r\n }\r\n\r\n get isCancelled(): boolean {\r\n return this._controller.signal.aborted;\r\n }\r\n\r\n static createLinkedToken(...tokens: CancellationToken[]): CancellationToken {\r\n const linked = new CancellationToken();\r\n for (const token of tokens) {\r\n token._linkedTokens.push(linked);\r\n if (token.isCancelled) {\r\n linked.cancel();\r\n return linked;\r\n }\r\n }\r\n return linked;\r\n }\r\n}\r\n\r\n// --- HttpClient ---\r\n\r\nexport class HttpClient {\r\n private readonly _options: HttpClientOptions;\r\n private readonly _interceptors: Interceptor[];\r\n\r\n constructor(options?: HttpClientOptions) {\r\n this._options = { ...options };\r\n this._interceptors = [];\r\n }\r\n\r\n get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('GET', url, options);\r\n }\r\n\r\n post<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('POST', url, { ...options, body: data } as RequestOptions & { body?: unknown });\r\n }\r\n\r\n put<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('PUT', url, { ...options, body: data } as RequestOptions & { body?: unknown });\r\n }\r\n\r\n delete<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('DELETE', url, options);\r\n }\r\n\r\n patch<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('PATCH', url, { ...options, body: data } as RequestOptions & { body?: unknown });\r\n }\r\n\r\n async request<T = unknown>(\r\n method: string,\r\n url: string,\r\n options?: RequestOptions & { body?: unknown },\r\n ): Promise<HttpResponse<T>> {\r\n let config = this._buildConfig(method, url, options);\r\n\r\n // Apply request interceptors\r\n for (const interceptor of this._interceptors) {\r\n if (interceptor.request) {\r\n config = await interceptor.request(config);\r\n }\r\n }\r\n\r\n try {\r\n const response = await this._fetch(config);\r\n let httpResponse = await this._parseResponse<T>(response, config.responseType);\r\n\r\n // Apply response interceptors\r\n for (const interceptor of this._interceptors) {\r\n if (interceptor.response) {\r\n httpResponse = (await interceptor.response(httpResponse as HttpResponse<unknown>)) as HttpResponse<T>;\r\n }\r\n }\r\n\r\n return httpResponse;\r\n } catch (err) {\r\n // Apply error interceptors\r\n for (const interceptor of this._interceptors) {\r\n if (interceptor.error) {\r\n const result = interceptor.error(err instanceof HttpError ? err : new HttpError(String(err)));\r\n if (result !== undefined) return result as HttpResponse<T>;\r\n }\r\n }\r\n throw err;\r\n }\r\n }\r\n\r\n use(interceptor: Interceptor): () => void {\r\n this._interceptors.push(interceptor);\r\n return () => this.eject(interceptor);\r\n }\r\n\r\n eject(interceptor: Interceptor): void {\r\n const index = this._interceptors.indexOf(interceptor);\r\n if (index !== -1) {\r\n this._interceptors.splice(index, 1);\r\n }\r\n }\r\n\r\n private _buildConfig(\r\n method: string,\r\n url: string,\r\n options?: RequestOptions & { body?: unknown },\r\n ): InternalRequestConfig {\r\n const { baseURL = '', headers: defaultHeaders = {} } = this._options;\r\n const resolvedURL = this._resolveURL(baseURL, url, options?.params);\r\n\r\n const headers: Record<string, string> = { ...defaultHeaders, ...options?.headers };\r\n\r\n let body: BodyInit | null = null;\r\n if (options && 'body' in options && options.body !== undefined) {\r\n if (typeof options.body === 'object' && options.body !== null) {\r\n body = JSON.stringify(options.body);\r\n if (!headers['Content-Type']) {\r\n headers['Content-Type'] = 'application/json';\r\n }\r\n } else {\r\n body = String(options.body);\r\n }\r\n }\r\n\r\n const timeout = options?.timeout ?? this._options.timeout;\r\n\r\n return {\r\n method: method.toUpperCase(),\r\n url: resolvedURL,\r\n headers,\r\n body,\r\n signal: options?.signal,\r\n responseType: options?.responseType,\r\n withCredentials: this._options.withCredentials,\r\n timeout,\r\n };\r\n }\r\n\r\n private _resolveURL(base: string, url: string, params?: Record<string, string>): string {\r\n let resolved = base ? base.replace(/\\/+$/, '') + '/' + url.replace(/^\\/+/, '') : url;\r\n if (params) {\r\n const searchParams = new URLSearchParams(params);\r\n const qs = searchParams.toString();\r\n if (qs) {\r\n resolved += (resolved.includes('?') ? '&' : '?') + qs;\r\n }\r\n }\r\n return resolved;\r\n }\r\n\r\n private async _fetch(config: InternalRequestConfig): Promise<Response> {\r\n const { method, url, headers, body, signal, withCredentials, timeout } = config;\r\n\r\n // Handle timeout via AbortController\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n let timeoutController: AbortController | undefined;\r\n\r\n if (timeout && timeout > 0) {\r\n timeoutController = new AbortController();\r\n timeoutId = setTimeout(() => timeoutController!.abort(), timeout);\r\n\r\n // Link with existing signal\r\n if (signal) {\r\n if (signal.aborted) {\r\n clearTimeout(timeoutId);\r\n throw new HttpError('Request was aborted', undefined);\r\n }\r\n signal.addEventListener('abort', () => {\r\n clearTimeout(timeoutId);\r\n timeoutController!.abort();\r\n }, { once: true });\r\n }\r\n }\r\n\r\n const fetchOptions: RequestInit = {\r\n method,\r\n headers,\r\n body,\r\n credentials: withCredentials ? 'include' : undefined,\r\n signal: timeoutController ? timeoutController.signal : signal,\r\n };\r\n\r\n try {\r\n const response = await fetch(url, fetchOptions);\r\n\r\n if (!response.ok) {\r\n const errorResponse: HttpResponse<unknown> = {\r\n data: null,\r\n status: response.status,\r\n statusText: response.statusText,\r\n headers: this._parseHeaders(response.headers),\r\n ok: false,\r\n };\r\n throw new HttpError(\r\n `HTTP ${response.status}: ${response.statusText}`,\r\n errorResponse,\r\n );\r\n }\r\n\r\n return response;\r\n } finally {\r\n if (timeoutId !== undefined) {\r\n clearTimeout(timeoutId);\r\n }\r\n }\r\n }\r\n\r\n private async _parseResponse<T>(\r\n response: Response,\r\n responseType?: 'json' | 'text' | 'blob' | 'arraybuffer',\r\n ): Promise<HttpResponse<T>> {\r\n let data: T;\r\n switch (responseType) {\r\n case 'text':\r\n data = (await response.text()) as unknown as T;\r\n break;\r\n case 'blob':\r\n data = (await response.blob()) as unknown as T;\r\n break;\r\n case 'arraybuffer':\r\n data = (await response.arrayBuffer()) as unknown as T;\r\n break;\r\n case 'json':\r\n default: {\r\n const text = await response.text();\r\n data = text ? JSON.parse(text) : (null as unknown as T);\r\n break;\r\n }\r\n }\r\n\r\n return {\r\n data,\r\n status: response.status,\r\n statusText: response.statusText,\r\n headers: this._parseHeaders(response.headers),\r\n ok: response.ok,\r\n };\r\n }\r\n\r\n private _parseHeaders(headers: Headers): Record<string, string> {\r\n const result: Record<string, string> = {};\r\n headers.forEach((value, key) => {\r\n result[key] = value;\r\n });\r\n return result;\r\n }\r\n}\r\n\r\n// --- Convenience exports ---\r\n\r\nexport function createHttpClient(options?: HttpClientOptions): HttpClient {\r\n return new HttpClient(options);\r\n}\r\n\r\nexport const http: HttpClient = new HttpClient();\r\n"]}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @lytjs/common-http
3
+ * Lightweight HTTP client based on native fetch API
4
+ */
5
+ interface HttpClientOptions {
6
+ baseURL?: string;
7
+ headers?: Record<string, string>;
8
+ timeout?: number;
9
+ withCredentials?: boolean;
10
+ }
11
+ interface RequestOptions {
12
+ headers?: Record<string, string>;
13
+ params?: Record<string, string>;
14
+ timeout?: number;
15
+ signal?: AbortSignal;
16
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
17
+ }
18
+ interface HttpResponse<T> {
19
+ data: T;
20
+ status: number;
21
+ statusText: string;
22
+ headers: Record<string, string>;
23
+ ok: boolean;
24
+ }
25
+ interface InternalRequestConfig {
26
+ method: string;
27
+ url: string;
28
+ headers: Record<string, string>;
29
+ body?: BodyInit | null;
30
+ signal?: AbortSignal;
31
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
32
+ withCredentials?: boolean;
33
+ timeout?: number;
34
+ }
35
+ interface Interceptor {
36
+ request?(config: InternalRequestConfig): InternalRequestConfig | Promise<InternalRequestConfig>;
37
+ response?(response: HttpResponse<unknown>): HttpResponse<unknown> | Promise<HttpResponse<unknown>>;
38
+ error?(error: HttpError): unknown;
39
+ }
40
+ declare class HttpError extends Error {
41
+ readonly response?: HttpResponse<unknown>;
42
+ readonly status?: number;
43
+ constructor(message: string, response?: HttpResponse<unknown>);
44
+ }
45
+ declare class CancellationToken {
46
+ private readonly _controller;
47
+ private readonly _linkedTokens;
48
+ constructor();
49
+ get signal(): AbortSignal;
50
+ cancel(): void;
51
+ get isCancelled(): boolean;
52
+ static createLinkedToken(...tokens: CancellationToken[]): CancellationToken;
53
+ }
54
+ declare class HttpClient {
55
+ private readonly _options;
56
+ private readonly _interceptors;
57
+ constructor(options?: HttpClientOptions);
58
+ get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>>;
59
+ post<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
60
+ put<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
61
+ delete<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>>;
62
+ patch<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
63
+ request<T = unknown>(method: string, url: string, options?: RequestOptions & {
64
+ body?: unknown;
65
+ }): Promise<HttpResponse<T>>;
66
+ use(interceptor: Interceptor): () => void;
67
+ eject(interceptor: Interceptor): void;
68
+ private _buildConfig;
69
+ private _resolveURL;
70
+ private _fetch;
71
+ private _parseResponse;
72
+ private _parseHeaders;
73
+ }
74
+ declare function createHttpClient(options?: HttpClientOptions): HttpClient;
75
+ declare const http: HttpClient;
76
+
77
+ export { CancellationToken, HttpClient, type HttpClientOptions, HttpError, type HttpResponse, type Interceptor, type InternalRequestConfig, type RequestOptions, createHttpClient, http };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @lytjs/common-http
3
+ * Lightweight HTTP client based on native fetch API
4
+ */
5
+ interface HttpClientOptions {
6
+ baseURL?: string;
7
+ headers?: Record<string, string>;
8
+ timeout?: number;
9
+ withCredentials?: boolean;
10
+ }
11
+ interface RequestOptions {
12
+ headers?: Record<string, string>;
13
+ params?: Record<string, string>;
14
+ timeout?: number;
15
+ signal?: AbortSignal;
16
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
17
+ }
18
+ interface HttpResponse<T> {
19
+ data: T;
20
+ status: number;
21
+ statusText: string;
22
+ headers: Record<string, string>;
23
+ ok: boolean;
24
+ }
25
+ interface InternalRequestConfig {
26
+ method: string;
27
+ url: string;
28
+ headers: Record<string, string>;
29
+ body?: BodyInit | null;
30
+ signal?: AbortSignal;
31
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
32
+ withCredentials?: boolean;
33
+ timeout?: number;
34
+ }
35
+ interface Interceptor {
36
+ request?(config: InternalRequestConfig): InternalRequestConfig | Promise<InternalRequestConfig>;
37
+ response?(response: HttpResponse<unknown>): HttpResponse<unknown> | Promise<HttpResponse<unknown>>;
38
+ error?(error: HttpError): unknown;
39
+ }
40
+ declare class HttpError extends Error {
41
+ readonly response?: HttpResponse<unknown>;
42
+ readonly status?: number;
43
+ constructor(message: string, response?: HttpResponse<unknown>);
44
+ }
45
+ declare class CancellationToken {
46
+ private readonly _controller;
47
+ private readonly _linkedTokens;
48
+ constructor();
49
+ get signal(): AbortSignal;
50
+ cancel(): void;
51
+ get isCancelled(): boolean;
52
+ static createLinkedToken(...tokens: CancellationToken[]): CancellationToken;
53
+ }
54
+ declare class HttpClient {
55
+ private readonly _options;
56
+ private readonly _interceptors;
57
+ constructor(options?: HttpClientOptions);
58
+ get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>>;
59
+ post<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
60
+ put<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
61
+ delete<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>>;
62
+ patch<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
63
+ request<T = unknown>(method: string, url: string, options?: RequestOptions & {
64
+ body?: unknown;
65
+ }): Promise<HttpResponse<T>>;
66
+ use(interceptor: Interceptor): () => void;
67
+ eject(interceptor: Interceptor): void;
68
+ private _buildConfig;
69
+ private _resolveURL;
70
+ private _fetch;
71
+ private _parseResponse;
72
+ private _parseHeaders;
73
+ }
74
+ declare function createHttpClient(options?: HttpClientOptions): HttpClient;
75
+ declare const http: HttpClient;
76
+
77
+ export { CancellationToken, HttpClient, type HttpClientOptions, HttpError, type HttpResponse, type Interceptor, type InternalRequestConfig, type RequestOptions, createHttpClient, http };
package/dist/index.mjs ADDED
@@ -0,0 +1,222 @@
1
+ // src/index.ts
2
+ var HttpError = class extends Error {
3
+ constructor(message, response) {
4
+ super(message);
5
+ this.name = "HttpError";
6
+ this.response = response;
7
+ this.status = response?.status;
8
+ }
9
+ };
10
+ var CancellationToken = class _CancellationToken {
11
+ constructor() {
12
+ this._controller = new AbortController();
13
+ this._linkedTokens = [];
14
+ }
15
+ get signal() {
16
+ return this._controller.signal;
17
+ }
18
+ cancel() {
19
+ this._controller.abort();
20
+ for (const token of this._linkedTokens) {
21
+ token.cancel();
22
+ }
23
+ }
24
+ get isCancelled() {
25
+ return this._controller.signal.aborted;
26
+ }
27
+ static createLinkedToken(...tokens) {
28
+ const linked = new _CancellationToken();
29
+ for (const token of tokens) {
30
+ token._linkedTokens.push(linked);
31
+ if (token.isCancelled) {
32
+ linked.cancel();
33
+ return linked;
34
+ }
35
+ }
36
+ return linked;
37
+ }
38
+ };
39
+ var HttpClient = class {
40
+ constructor(options) {
41
+ this._options = { ...options };
42
+ this._interceptors = [];
43
+ }
44
+ get(url, options) {
45
+ return this.request("GET", url, options);
46
+ }
47
+ post(url, data, options) {
48
+ return this.request("POST", url, { ...options, body: data });
49
+ }
50
+ put(url, data, options) {
51
+ return this.request("PUT", url, { ...options, body: data });
52
+ }
53
+ delete(url, options) {
54
+ return this.request("DELETE", url, options);
55
+ }
56
+ patch(url, data, options) {
57
+ return this.request("PATCH", url, { ...options, body: data });
58
+ }
59
+ async request(method, url, options) {
60
+ let config = this._buildConfig(method, url, options);
61
+ for (const interceptor of this._interceptors) {
62
+ if (interceptor.request) {
63
+ config = await interceptor.request(config);
64
+ }
65
+ }
66
+ try {
67
+ const response = await this._fetch(config);
68
+ let httpResponse = await this._parseResponse(response, config.responseType);
69
+ for (const interceptor of this._interceptors) {
70
+ if (interceptor.response) {
71
+ httpResponse = await interceptor.response(httpResponse);
72
+ }
73
+ }
74
+ return httpResponse;
75
+ } catch (err) {
76
+ for (const interceptor of this._interceptors) {
77
+ if (interceptor.error) {
78
+ const result = interceptor.error(err instanceof HttpError ? err : new HttpError(String(err)));
79
+ if (result !== void 0) return result;
80
+ }
81
+ }
82
+ throw err;
83
+ }
84
+ }
85
+ use(interceptor) {
86
+ this._interceptors.push(interceptor);
87
+ return () => this.eject(interceptor);
88
+ }
89
+ eject(interceptor) {
90
+ const index = this._interceptors.indexOf(interceptor);
91
+ if (index !== -1) {
92
+ this._interceptors.splice(index, 1);
93
+ }
94
+ }
95
+ _buildConfig(method, url, options) {
96
+ const { baseURL = "", headers: defaultHeaders = {} } = this._options;
97
+ const resolvedURL = this._resolveURL(baseURL, url, options?.params);
98
+ const headers = { ...defaultHeaders, ...options?.headers };
99
+ let body = null;
100
+ if (options && "body" in options && options.body !== void 0) {
101
+ if (typeof options.body === "object" && options.body !== null) {
102
+ body = JSON.stringify(options.body);
103
+ if (!headers["Content-Type"]) {
104
+ headers["Content-Type"] = "application/json";
105
+ }
106
+ } else {
107
+ body = String(options.body);
108
+ }
109
+ }
110
+ const timeout = options?.timeout ?? this._options.timeout;
111
+ return {
112
+ method: method.toUpperCase(),
113
+ url: resolvedURL,
114
+ headers,
115
+ body,
116
+ signal: options?.signal,
117
+ responseType: options?.responseType,
118
+ withCredentials: this._options.withCredentials,
119
+ timeout
120
+ };
121
+ }
122
+ _resolveURL(base, url, params) {
123
+ let resolved = base ? base.replace(/\/+$/, "") + "/" + url.replace(/^\/+/, "") : url;
124
+ if (params) {
125
+ const searchParams = new URLSearchParams(params);
126
+ const qs = searchParams.toString();
127
+ if (qs) {
128
+ resolved += (resolved.includes("?") ? "&" : "?") + qs;
129
+ }
130
+ }
131
+ return resolved;
132
+ }
133
+ async _fetch(config) {
134
+ const { method, url, headers, body, signal, withCredentials, timeout } = config;
135
+ let timeoutId;
136
+ let timeoutController;
137
+ if (timeout && timeout > 0) {
138
+ timeoutController = new AbortController();
139
+ timeoutId = setTimeout(() => timeoutController.abort(), timeout);
140
+ if (signal) {
141
+ if (signal.aborted) {
142
+ clearTimeout(timeoutId);
143
+ throw new HttpError("Request was aborted", void 0);
144
+ }
145
+ signal.addEventListener("abort", () => {
146
+ clearTimeout(timeoutId);
147
+ timeoutController.abort();
148
+ }, { once: true });
149
+ }
150
+ }
151
+ const fetchOptions = {
152
+ method,
153
+ headers,
154
+ body,
155
+ credentials: withCredentials ? "include" : void 0,
156
+ signal: timeoutController ? timeoutController.signal : signal
157
+ };
158
+ try {
159
+ const response = await fetch(url, fetchOptions);
160
+ if (!response.ok) {
161
+ const errorResponse = {
162
+ data: null,
163
+ status: response.status,
164
+ statusText: response.statusText,
165
+ headers: this._parseHeaders(response.headers),
166
+ ok: false
167
+ };
168
+ throw new HttpError(
169
+ `HTTP ${response.status}: ${response.statusText}`,
170
+ errorResponse
171
+ );
172
+ }
173
+ return response;
174
+ } finally {
175
+ if (timeoutId !== void 0) {
176
+ clearTimeout(timeoutId);
177
+ }
178
+ }
179
+ }
180
+ async _parseResponse(response, responseType) {
181
+ let data;
182
+ switch (responseType) {
183
+ case "text":
184
+ data = await response.text();
185
+ break;
186
+ case "blob":
187
+ data = await response.blob();
188
+ break;
189
+ case "arraybuffer":
190
+ data = await response.arrayBuffer();
191
+ break;
192
+ case "json":
193
+ default: {
194
+ const text = await response.text();
195
+ data = text ? JSON.parse(text) : null;
196
+ break;
197
+ }
198
+ }
199
+ return {
200
+ data,
201
+ status: response.status,
202
+ statusText: response.statusText,
203
+ headers: this._parseHeaders(response.headers),
204
+ ok: response.ok
205
+ };
206
+ }
207
+ _parseHeaders(headers) {
208
+ const result = {};
209
+ headers.forEach((value, key) => {
210
+ result[key] = value;
211
+ });
212
+ return result;
213
+ }
214
+ };
215
+ function createHttpClient(options) {
216
+ return new HttpClient(options);
217
+ }
218
+ var http = new HttpClient();
219
+
220
+ export { CancellationToken, HttpClient, HttpError, createHttpClient, http };
221
+ //# sourceMappingURL=index.mjs.map
222
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\r\n * @lytjs/common-http\r\n * Lightweight HTTP client based on native fetch API\r\n */\r\n\r\n// --- Types ---\r\n\r\nexport interface HttpClientOptions {\r\n baseURL?: string;\r\n headers?: Record<string, string>;\r\n timeout?: number;\r\n withCredentials?: boolean;\r\n}\r\n\r\nexport interface RequestOptions {\r\n headers?: Record<string, string>;\r\n params?: Record<string, string>;\r\n timeout?: number;\r\n signal?: AbortSignal;\r\n responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';\r\n}\r\n\r\nexport interface HttpResponse<T> {\r\n data: T;\r\n status: number;\r\n statusText: string;\r\n headers: Record<string, string>;\r\n ok: boolean;\r\n}\r\n\r\nexport interface InternalRequestConfig {\r\n method: string;\r\n url: string;\r\n headers: Record<string, string>;\r\n body?: BodyInit | null;\r\n signal?: AbortSignal;\r\n responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';\r\n withCredentials?: boolean;\r\n timeout?: number;\r\n}\r\n\r\nexport interface Interceptor {\r\n request?(config: InternalRequestConfig): InternalRequestConfig | Promise<InternalRequestConfig>;\r\n response?(response: HttpResponse<unknown>): HttpResponse<unknown> | Promise<HttpResponse<unknown>>;\r\n error?(error: HttpError): unknown;\r\n}\r\n\r\n// --- HttpError ---\r\n\r\nexport class HttpError extends Error {\r\n readonly response?: HttpResponse<unknown>;\r\n readonly status?: number;\r\n\r\n constructor(message: string, response?: HttpResponse<unknown>) {\r\n super(message);\r\n this.name = 'HttpError';\r\n this.response = response;\r\n this.status = response?.status;\r\n }\r\n}\r\n\r\n// --- CancellationToken ---\r\n\r\nexport class CancellationToken {\r\n private readonly _controller: AbortController;\r\n private readonly _linkedTokens: CancellationToken[];\r\n\r\n constructor() {\r\n this._controller = new AbortController();\r\n this._linkedTokens = [];\r\n }\r\n\r\n get signal(): AbortSignal {\r\n return this._controller.signal;\r\n }\r\n\r\n cancel(): void {\r\n this._controller.abort();\r\n for (const token of this._linkedTokens) {\r\n token.cancel();\r\n }\r\n }\r\n\r\n get isCancelled(): boolean {\r\n return this._controller.signal.aborted;\r\n }\r\n\r\n static createLinkedToken(...tokens: CancellationToken[]): CancellationToken {\r\n const linked = new CancellationToken();\r\n for (const token of tokens) {\r\n token._linkedTokens.push(linked);\r\n if (token.isCancelled) {\r\n linked.cancel();\r\n return linked;\r\n }\r\n }\r\n return linked;\r\n }\r\n}\r\n\r\n// --- HttpClient ---\r\n\r\nexport class HttpClient {\r\n private readonly _options: HttpClientOptions;\r\n private readonly _interceptors: Interceptor[];\r\n\r\n constructor(options?: HttpClientOptions) {\r\n this._options = { ...options };\r\n this._interceptors = [];\r\n }\r\n\r\n get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('GET', url, options);\r\n }\r\n\r\n post<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('POST', url, { ...options, body: data } as RequestOptions & { body?: unknown });\r\n }\r\n\r\n put<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('PUT', url, { ...options, body: data } as RequestOptions & { body?: unknown });\r\n }\r\n\r\n delete<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('DELETE', url, options);\r\n }\r\n\r\n patch<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {\r\n return this.request<T>('PATCH', url, { ...options, body: data } as RequestOptions & { body?: unknown });\r\n }\r\n\r\n async request<T = unknown>(\r\n method: string,\r\n url: string,\r\n options?: RequestOptions & { body?: unknown },\r\n ): Promise<HttpResponse<T>> {\r\n let config = this._buildConfig(method, url, options);\r\n\r\n // Apply request interceptors\r\n for (const interceptor of this._interceptors) {\r\n if (interceptor.request) {\r\n config = await interceptor.request(config);\r\n }\r\n }\r\n\r\n try {\r\n const response = await this._fetch(config);\r\n let httpResponse = await this._parseResponse<T>(response, config.responseType);\r\n\r\n // Apply response interceptors\r\n for (const interceptor of this._interceptors) {\r\n if (interceptor.response) {\r\n httpResponse = (await interceptor.response(httpResponse as HttpResponse<unknown>)) as HttpResponse<T>;\r\n }\r\n }\r\n\r\n return httpResponse;\r\n } catch (err) {\r\n // Apply error interceptors\r\n for (const interceptor of this._interceptors) {\r\n if (interceptor.error) {\r\n const result = interceptor.error(err instanceof HttpError ? err : new HttpError(String(err)));\r\n if (result !== undefined) return result as HttpResponse<T>;\r\n }\r\n }\r\n throw err;\r\n }\r\n }\r\n\r\n use(interceptor: Interceptor): () => void {\r\n this._interceptors.push(interceptor);\r\n return () => this.eject(interceptor);\r\n }\r\n\r\n eject(interceptor: Interceptor): void {\r\n const index = this._interceptors.indexOf(interceptor);\r\n if (index !== -1) {\r\n this._interceptors.splice(index, 1);\r\n }\r\n }\r\n\r\n private _buildConfig(\r\n method: string,\r\n url: string,\r\n options?: RequestOptions & { body?: unknown },\r\n ): InternalRequestConfig {\r\n const { baseURL = '', headers: defaultHeaders = {} } = this._options;\r\n const resolvedURL = this._resolveURL(baseURL, url, options?.params);\r\n\r\n const headers: Record<string, string> = { ...defaultHeaders, ...options?.headers };\r\n\r\n let body: BodyInit | null = null;\r\n if (options && 'body' in options && options.body !== undefined) {\r\n if (typeof options.body === 'object' && options.body !== null) {\r\n body = JSON.stringify(options.body);\r\n if (!headers['Content-Type']) {\r\n headers['Content-Type'] = 'application/json';\r\n }\r\n } else {\r\n body = String(options.body);\r\n }\r\n }\r\n\r\n const timeout = options?.timeout ?? this._options.timeout;\r\n\r\n return {\r\n method: method.toUpperCase(),\r\n url: resolvedURL,\r\n headers,\r\n body,\r\n signal: options?.signal,\r\n responseType: options?.responseType,\r\n withCredentials: this._options.withCredentials,\r\n timeout,\r\n };\r\n }\r\n\r\n private _resolveURL(base: string, url: string, params?: Record<string, string>): string {\r\n let resolved = base ? base.replace(/\\/+$/, '') + '/' + url.replace(/^\\/+/, '') : url;\r\n if (params) {\r\n const searchParams = new URLSearchParams(params);\r\n const qs = searchParams.toString();\r\n if (qs) {\r\n resolved += (resolved.includes('?') ? '&' : '?') + qs;\r\n }\r\n }\r\n return resolved;\r\n }\r\n\r\n private async _fetch(config: InternalRequestConfig): Promise<Response> {\r\n const { method, url, headers, body, signal, withCredentials, timeout } = config;\r\n\r\n // Handle timeout via AbortController\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n let timeoutController: AbortController | undefined;\r\n\r\n if (timeout && timeout > 0) {\r\n timeoutController = new AbortController();\r\n timeoutId = setTimeout(() => timeoutController!.abort(), timeout);\r\n\r\n // Link with existing signal\r\n if (signal) {\r\n if (signal.aborted) {\r\n clearTimeout(timeoutId);\r\n throw new HttpError('Request was aborted', undefined);\r\n }\r\n signal.addEventListener('abort', () => {\r\n clearTimeout(timeoutId);\r\n timeoutController!.abort();\r\n }, { once: true });\r\n }\r\n }\r\n\r\n const fetchOptions: RequestInit = {\r\n method,\r\n headers,\r\n body,\r\n credentials: withCredentials ? 'include' : undefined,\r\n signal: timeoutController ? timeoutController.signal : signal,\r\n };\r\n\r\n try {\r\n const response = await fetch(url, fetchOptions);\r\n\r\n if (!response.ok) {\r\n const errorResponse: HttpResponse<unknown> = {\r\n data: null,\r\n status: response.status,\r\n statusText: response.statusText,\r\n headers: this._parseHeaders(response.headers),\r\n ok: false,\r\n };\r\n throw new HttpError(\r\n `HTTP ${response.status}: ${response.statusText}`,\r\n errorResponse,\r\n );\r\n }\r\n\r\n return response;\r\n } finally {\r\n if (timeoutId !== undefined) {\r\n clearTimeout(timeoutId);\r\n }\r\n }\r\n }\r\n\r\n private async _parseResponse<T>(\r\n response: Response,\r\n responseType?: 'json' | 'text' | 'blob' | 'arraybuffer',\r\n ): Promise<HttpResponse<T>> {\r\n let data: T;\r\n switch (responseType) {\r\n case 'text':\r\n data = (await response.text()) as unknown as T;\r\n break;\r\n case 'blob':\r\n data = (await response.blob()) as unknown as T;\r\n break;\r\n case 'arraybuffer':\r\n data = (await response.arrayBuffer()) as unknown as T;\r\n break;\r\n case 'json':\r\n default: {\r\n const text = await response.text();\r\n data = text ? JSON.parse(text) : (null as unknown as T);\r\n break;\r\n }\r\n }\r\n\r\n return {\r\n data,\r\n status: response.status,\r\n statusText: response.statusText,\r\n headers: this._parseHeaders(response.headers),\r\n ok: response.ok,\r\n };\r\n }\r\n\r\n private _parseHeaders(headers: Headers): Record<string, string> {\r\n const result: Record<string, string> = {};\r\n headers.forEach((value, key) => {\r\n result[key] = value;\r\n });\r\n return result;\r\n }\r\n}\r\n\r\n// --- Convenience exports ---\r\n\r\nexport function createHttpClient(options?: HttpClientOptions): HttpClient {\r\n return new HttpClient(options);\r\n}\r\n\r\nexport const http: HttpClient = new HttpClient();\r\n"]}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lytjs/common-http",
3
+ "version": "6.0.0",
4
+ "description": "Lightweight HTTP client for LytJS based on native fetch API",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage",
23
+ "lint": "eslint \"src/**/*.ts\"",
24
+ "build": "tsup",
25
+ "type-check": "tsc --noEmit",
26
+ "clean": "rm -rf dist"
27
+ },
28
+ "sideEffects": false,
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "tsup": "^8.4.0",
32
+ "typescript": "^5.8.2",
33
+ "vitest": "^3.0.7"
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * @lytjs/common-http
3
+ * Lightweight HTTP client based on native fetch API
4
+ */
5
+
6
+ // --- Types ---
7
+
8
+ export interface HttpClientOptions {
9
+ baseURL?: string;
10
+ headers?: Record<string, string>;
11
+ timeout?: number;
12
+ withCredentials?: boolean;
13
+ }
14
+
15
+ export interface RequestOptions {
16
+ headers?: Record<string, string>;
17
+ params?: Record<string, string>;
18
+ timeout?: number;
19
+ signal?: AbortSignal;
20
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
21
+ }
22
+
23
+ export interface HttpResponse<T> {
24
+ data: T;
25
+ status: number;
26
+ statusText: string;
27
+ headers: Record<string, string>;
28
+ ok: boolean;
29
+ }
30
+
31
+ export interface InternalRequestConfig {
32
+ method: string;
33
+ url: string;
34
+ headers: Record<string, string>;
35
+ body?: BodyInit | null;
36
+ signal?: AbortSignal;
37
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
38
+ withCredentials?: boolean;
39
+ timeout?: number;
40
+ }
41
+
42
+ export interface Interceptor {
43
+ request?(config: InternalRequestConfig): InternalRequestConfig | Promise<InternalRequestConfig>;
44
+ response?(response: HttpResponse<unknown>): HttpResponse<unknown> | Promise<HttpResponse<unknown>>;
45
+ error?(error: HttpError): unknown;
46
+ }
47
+
48
+ // --- HttpError ---
49
+
50
+ export class HttpError extends Error {
51
+ readonly response?: HttpResponse<unknown>;
52
+ readonly status?: number;
53
+
54
+ constructor(message: string, response?: HttpResponse<unknown>) {
55
+ super(message);
56
+ this.name = 'HttpError';
57
+ this.response = response;
58
+ this.status = response?.status;
59
+ }
60
+ }
61
+
62
+ // --- CancellationToken ---
63
+
64
+ export class CancellationToken {
65
+ private readonly _controller: AbortController;
66
+ private readonly _linkedTokens: CancellationToken[];
67
+
68
+ constructor() {
69
+ this._controller = new AbortController();
70
+ this._linkedTokens = [];
71
+ }
72
+
73
+ get signal(): AbortSignal {
74
+ return this._controller.signal;
75
+ }
76
+
77
+ cancel(): void {
78
+ this._controller.abort();
79
+ for (const token of this._linkedTokens) {
80
+ token.cancel();
81
+ }
82
+ }
83
+
84
+ get isCancelled(): boolean {
85
+ return this._controller.signal.aborted;
86
+ }
87
+
88
+ static createLinkedToken(...tokens: CancellationToken[]): CancellationToken {
89
+ const linked = new CancellationToken();
90
+ for (const token of tokens) {
91
+ token._linkedTokens.push(linked);
92
+ if (token.isCancelled) {
93
+ linked.cancel();
94
+ return linked;
95
+ }
96
+ }
97
+ return linked;
98
+ }
99
+ }
100
+
101
+ // --- HttpClient ---
102
+
103
+ export class HttpClient {
104
+ private readonly _options: HttpClientOptions;
105
+ private readonly _interceptors: Interceptor[];
106
+
107
+ constructor(options?: HttpClientOptions) {
108
+ this._options = { ...options };
109
+ this._interceptors = [];
110
+ }
111
+
112
+ get<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
113
+ return this.request<T>('GET', url, options);
114
+ }
115
+
116
+ post<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {
117
+ return this.request<T>('POST', url, { ...options, body: data } as RequestOptions & { body?: unknown });
118
+ }
119
+
120
+ put<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {
121
+ return this.request<T>('PUT', url, { ...options, body: data } as RequestOptions & { body?: unknown });
122
+ }
123
+
124
+ delete<T = unknown>(url: string, options?: RequestOptions): Promise<HttpResponse<T>> {
125
+ return this.request<T>('DELETE', url, options);
126
+ }
127
+
128
+ patch<T = unknown>(url: string, data?: unknown, options?: RequestOptions): Promise<HttpResponse<T>> {
129
+ return this.request<T>('PATCH', url, { ...options, body: data } as RequestOptions & { body?: unknown });
130
+ }
131
+
132
+ async request<T = unknown>(
133
+ method: string,
134
+ url: string,
135
+ options?: RequestOptions & { body?: unknown },
136
+ ): Promise<HttpResponse<T>> {
137
+ let config = this._buildConfig(method, url, options);
138
+
139
+ // Apply request interceptors
140
+ for (const interceptor of this._interceptors) {
141
+ if (interceptor.request) {
142
+ config = await interceptor.request(config);
143
+ }
144
+ }
145
+
146
+ try {
147
+ const response = await this._fetch(config);
148
+ let httpResponse = await this._parseResponse<T>(response, config.responseType);
149
+
150
+ // Apply response interceptors
151
+ for (const interceptor of this._interceptors) {
152
+ if (interceptor.response) {
153
+ httpResponse = (await interceptor.response(httpResponse as HttpResponse<unknown>)) as HttpResponse<T>;
154
+ }
155
+ }
156
+
157
+ return httpResponse;
158
+ } catch (err) {
159
+ // Apply error interceptors
160
+ for (const interceptor of this._interceptors) {
161
+ if (interceptor.error) {
162
+ const result = interceptor.error(err instanceof HttpError ? err : new HttpError(String(err)));
163
+ if (result !== undefined) return result as HttpResponse<T>;
164
+ }
165
+ }
166
+ throw err;
167
+ }
168
+ }
169
+
170
+ use(interceptor: Interceptor): () => void {
171
+ this._interceptors.push(interceptor);
172
+ return () => this.eject(interceptor);
173
+ }
174
+
175
+ eject(interceptor: Interceptor): void {
176
+ const index = this._interceptors.indexOf(interceptor);
177
+ if (index !== -1) {
178
+ this._interceptors.splice(index, 1);
179
+ }
180
+ }
181
+
182
+ private _buildConfig(
183
+ method: string,
184
+ url: string,
185
+ options?: RequestOptions & { body?: unknown },
186
+ ): InternalRequestConfig {
187
+ const { baseURL = '', headers: defaultHeaders = {} } = this._options;
188
+ const resolvedURL = this._resolveURL(baseURL, url, options?.params);
189
+
190
+ const headers: Record<string, string> = { ...defaultHeaders, ...options?.headers };
191
+
192
+ let body: BodyInit | null = null;
193
+ if (options && 'body' in options && options.body !== undefined) {
194
+ if (typeof options.body === 'object' && options.body !== null) {
195
+ body = JSON.stringify(options.body);
196
+ if (!headers['Content-Type']) {
197
+ headers['Content-Type'] = 'application/json';
198
+ }
199
+ } else {
200
+ body = String(options.body);
201
+ }
202
+ }
203
+
204
+ const timeout = options?.timeout ?? this._options.timeout;
205
+
206
+ return {
207
+ method: method.toUpperCase(),
208
+ url: resolvedURL,
209
+ headers,
210
+ body,
211
+ signal: options?.signal,
212
+ responseType: options?.responseType,
213
+ withCredentials: this._options.withCredentials,
214
+ timeout,
215
+ };
216
+ }
217
+
218
+ private _resolveURL(base: string, url: string, params?: Record<string, string>): string {
219
+ let resolved = base ? base.replace(/\/+$/, '') + '/' + url.replace(/^\/+/, '') : url;
220
+ if (params) {
221
+ const searchParams = new URLSearchParams(params);
222
+ const qs = searchParams.toString();
223
+ if (qs) {
224
+ resolved += (resolved.includes('?') ? '&' : '?') + qs;
225
+ }
226
+ }
227
+ return resolved;
228
+ }
229
+
230
+ private async _fetch(config: InternalRequestConfig): Promise<Response> {
231
+ const { method, url, headers, body, signal, withCredentials, timeout } = config;
232
+
233
+ // Handle timeout via AbortController
234
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
235
+ let timeoutController: AbortController | undefined;
236
+
237
+ if (timeout && timeout > 0) {
238
+ timeoutController = new AbortController();
239
+ timeoutId = setTimeout(() => timeoutController!.abort(), timeout);
240
+
241
+ // Link with existing signal
242
+ if (signal) {
243
+ if (signal.aborted) {
244
+ clearTimeout(timeoutId);
245
+ throw new HttpError('Request was aborted', undefined);
246
+ }
247
+ signal.addEventListener('abort', () => {
248
+ clearTimeout(timeoutId);
249
+ timeoutController!.abort();
250
+ }, { once: true });
251
+ }
252
+ }
253
+
254
+ const fetchOptions: RequestInit = {
255
+ method,
256
+ headers,
257
+ body,
258
+ credentials: withCredentials ? 'include' : undefined,
259
+ signal: timeoutController ? timeoutController.signal : signal,
260
+ };
261
+
262
+ try {
263
+ const response = await fetch(url, fetchOptions);
264
+
265
+ if (!response.ok) {
266
+ const errorResponse: HttpResponse<unknown> = {
267
+ data: null,
268
+ status: response.status,
269
+ statusText: response.statusText,
270
+ headers: this._parseHeaders(response.headers),
271
+ ok: false,
272
+ };
273
+ throw new HttpError(
274
+ `HTTP ${response.status}: ${response.statusText}`,
275
+ errorResponse,
276
+ );
277
+ }
278
+
279
+ return response;
280
+ } finally {
281
+ if (timeoutId !== undefined) {
282
+ clearTimeout(timeoutId);
283
+ }
284
+ }
285
+ }
286
+
287
+ private async _parseResponse<T>(
288
+ response: Response,
289
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer',
290
+ ): Promise<HttpResponse<T>> {
291
+ let data: T;
292
+ switch (responseType) {
293
+ case 'text':
294
+ data = (await response.text()) as unknown as T;
295
+ break;
296
+ case 'blob':
297
+ data = (await response.blob()) as unknown as T;
298
+ break;
299
+ case 'arraybuffer':
300
+ data = (await response.arrayBuffer()) as unknown as T;
301
+ break;
302
+ case 'json':
303
+ default: {
304
+ const text = await response.text();
305
+ data = text ? JSON.parse(text) : (null as unknown as T);
306
+ break;
307
+ }
308
+ }
309
+
310
+ return {
311
+ data,
312
+ status: response.status,
313
+ statusText: response.statusText,
314
+ headers: this._parseHeaders(response.headers),
315
+ ok: response.ok,
316
+ };
317
+ }
318
+
319
+ private _parseHeaders(headers: Headers): Record<string, string> {
320
+ const result: Record<string, string> = {};
321
+ headers.forEach((value, key) => {
322
+ result[key] = value;
323
+ });
324
+ return result;
325
+ }
326
+ }
327
+
328
+ // --- Convenience exports ---
329
+
330
+ export function createHttpClient(options?: HttpClientOptions): HttpClient {
331
+ return new HttpClient(options);
332
+ }
333
+
334
+ export const http: HttpClient = new HttpClient();