@afoures/http-client 0.2.0 → 0.4.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.d.mts +2 -2
- package/dist/lib/endpoint.d.mts +1 -1
- package/dist/lib/endpoint.mjs +105 -54
- package/dist/lib/errors.d.mts +57 -33
- package/dist/lib/errors.mjs +22 -24
- package/dist/lib/http-client.d.mts +33 -2
- package/dist/lib/http-client.mjs +133 -13
- package/dist/lib/types.d.mts +2 -0
- package/docs/http-client.md +43 -0
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbortedError, HttpClientError, NetworkError, ParseError, SerializationError, TimeoutError, UnexpectedError } from "./lib/errors.mjs";
|
|
2
2
|
import { HTTPFetch, HTTPMethod, HTTPStatus, Parser, Pathname, Schema, Serializer } from "./lib/types.mjs";
|
|
3
3
|
import { AnyEndpoint, Endpoint, EndpointDefinition } from "./lib/endpoint.mjs";
|
|
4
|
-
import { EndpointMap, HttpClientOptions, http_client } from "./lib/http-client.mjs";
|
|
5
|
-
export { AbortedError, type AnyEndpoint, Endpoint, type EndpointDefinition, type EndpointMap, type HTTPFetch, type HTTPMethod, type HTTPStatus, HttpClientError, type HttpClientOptions, NetworkError, ParseError, type Parser, type Pathname, type Schema, SerializationError, type Serializer, TimeoutError, UnexpectedError, http_client };
|
|
4
|
+
import { $infer, EndpointMap, HttpClientOptions, http_client } from "./lib/http-client.mjs";
|
|
5
|
+
export { type $infer, AbortedError, type AnyEndpoint, Endpoint, type EndpointDefinition, type EndpointMap, type HTTPFetch, type HTTPMethod, type HTTPStatus, HttpClientError, type HttpClientOptions, NetworkError, ParseError, type Parser, type Pathname, type Schema, SerializationError, type Serializer, TimeoutError, UnexpectedError, http_client };
|
package/dist/lib/endpoint.d.mts
CHANGED
|
@@ -33,7 +33,7 @@ declare class Endpoint<http_method extends HTTPMethod.Any, pathname extends Path
|
|
|
33
33
|
body: BodyInit | null;
|
|
34
34
|
content_type?: string;
|
|
35
35
|
} | SerializationError>;
|
|
36
|
-
parse_response(
|
|
36
|
+
parse_response(raw_response: Response): Promise<HTTPFetch.ClientErrorResponse<Schema.infer_output<error_schema, string>> | HTTPFetch.ServerErrorResponse<Schema.infer_output<error_schema, string>> | HTTPFetch.SuccessfulResponse<Schema.infer_output<data_schema, void>> | HTTPFetch.RedirectMessage | ParseError>;
|
|
37
37
|
}
|
|
38
38
|
type AnyEndpoint = Endpoint<any, any, any, any, any, any, any>;
|
|
39
39
|
//#endregion
|
package/dist/lib/endpoint.mjs
CHANGED
|
@@ -3,6 +3,77 @@ import "./types.mjs";
|
|
|
3
3
|
import { RoutePattern } from "@remix-run/route-pattern";
|
|
4
4
|
|
|
5
5
|
//#region src/lib/endpoint.ts
|
|
6
|
+
const RESPONSE = {
|
|
7
|
+
success(method, data, raw_response) {
|
|
8
|
+
const response = {
|
|
9
|
+
ok: true,
|
|
10
|
+
method,
|
|
11
|
+
url: raw_response.url,
|
|
12
|
+
status: raw_response.status,
|
|
13
|
+
data,
|
|
14
|
+
headers: raw_response.headers,
|
|
15
|
+
raw_response
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(response, "raw_response", {
|
|
18
|
+
enumerable: false,
|
|
19
|
+
writable: false,
|
|
20
|
+
configurable: false
|
|
21
|
+
});
|
|
22
|
+
return response;
|
|
23
|
+
},
|
|
24
|
+
redirect(method, raw_response) {
|
|
25
|
+
const redirect_to = raw_response.headers.get("Location") || null;
|
|
26
|
+
const response = {
|
|
27
|
+
ok: false,
|
|
28
|
+
method,
|
|
29
|
+
url: raw_response.url,
|
|
30
|
+
status: raw_response.status,
|
|
31
|
+
redirect_to,
|
|
32
|
+
headers: raw_response.headers,
|
|
33
|
+
raw_response
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(response, "raw_response", {
|
|
36
|
+
enumerable: false,
|
|
37
|
+
writable: false,
|
|
38
|
+
configurable: false
|
|
39
|
+
});
|
|
40
|
+
return response;
|
|
41
|
+
},
|
|
42
|
+
client_error(method, error, raw_response) {
|
|
43
|
+
const response = {
|
|
44
|
+
ok: false,
|
|
45
|
+
method,
|
|
46
|
+
url: raw_response.url,
|
|
47
|
+
status: raw_response.status,
|
|
48
|
+
error,
|
|
49
|
+
headers: raw_response.headers,
|
|
50
|
+
raw_response
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(response, "raw_response", {
|
|
53
|
+
enumerable: false,
|
|
54
|
+
writable: false,
|
|
55
|
+
configurable: false
|
|
56
|
+
});
|
|
57
|
+
return response;
|
|
58
|
+
},
|
|
59
|
+
server_error(method, error, raw_response) {
|
|
60
|
+
const response = {
|
|
61
|
+
ok: false,
|
|
62
|
+
method,
|
|
63
|
+
url: raw_response.url,
|
|
64
|
+
status: raw_response.status,
|
|
65
|
+
error,
|
|
66
|
+
headers: raw_response.headers,
|
|
67
|
+
raw_response
|
|
68
|
+
};
|
|
69
|
+
Object.defineProperty(response, "raw_response", {
|
|
70
|
+
enumerable: false,
|
|
71
|
+
writable: false,
|
|
72
|
+
configurable: false
|
|
73
|
+
});
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
6
77
|
var Endpoint = class {
|
|
7
78
|
#method;
|
|
8
79
|
#pattern;
|
|
@@ -35,7 +106,8 @@ var Endpoint = class {
|
|
|
35
106
|
const result = await this.#serializers.params.schema["~standard"].validate(init.params);
|
|
36
107
|
if (result.issues !== void 0) return new SerializationError("Params serialization failed", {
|
|
37
108
|
operation: "generate_url",
|
|
38
|
-
cause: result.issues
|
|
109
|
+
cause: result.issues,
|
|
110
|
+
input: { params: init.params }
|
|
39
111
|
});
|
|
40
112
|
const transformed_params = result.value;
|
|
41
113
|
if (this.#serializers.params.serialize) pathname_params = this.#serializers.params.serialize(transformed_params);
|
|
@@ -47,7 +119,8 @@ var Endpoint = class {
|
|
|
47
119
|
const result = await this.#serializers.query.schema["~standard"].validate(init.query);
|
|
48
120
|
if (result.issues !== void 0) return new SerializationError("Query serialization failed", {
|
|
49
121
|
cause: result.issues,
|
|
50
|
-
operation: "generate_url"
|
|
122
|
+
operation: "generate_url",
|
|
123
|
+
input: { query: init.query }
|
|
51
124
|
});
|
|
52
125
|
const transformed_query = result.value;
|
|
53
126
|
if (typeof this.#serializers.query.serialize === "function") search_params = this.#serializers.query.serialize(transformed_query);
|
|
@@ -80,7 +153,8 @@ var Endpoint = class {
|
|
|
80
153
|
const result = await this.#serializers.body.schema["~standard"].validate(init.body);
|
|
81
154
|
if (result.issues !== void 0) return new SerializationError("Body serialization failed", {
|
|
82
155
|
operation: "serialize_body",
|
|
83
|
-
cause: result.issues
|
|
156
|
+
cause: result.issues,
|
|
157
|
+
input: { body: init.body }
|
|
84
158
|
});
|
|
85
159
|
const transformed_content = result.value;
|
|
86
160
|
if (typeof this.#serializers.body.serialize === "function") return this.#serializers.body.serialize(transformed_content);
|
|
@@ -89,74 +163,51 @@ var Endpoint = class {
|
|
|
89
163
|
content_type: "application/json"
|
|
90
164
|
};
|
|
91
165
|
}
|
|
92
|
-
async parse_response(
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const headers = cloned_response.headers;
|
|
97
|
-
if (status >= 300 && status < 400) return {
|
|
98
|
-
ok: false,
|
|
99
|
-
status,
|
|
100
|
-
redirect_to: headers.get("Location") || null,
|
|
101
|
-
headers,
|
|
102
|
-
raw_response
|
|
103
|
-
};
|
|
104
|
-
if (status >= 400 && status < 600) {
|
|
166
|
+
async parse_response(raw_response) {
|
|
167
|
+
const response = raw_response.clone();
|
|
168
|
+
if (raw_response.status >= 300 && raw_response.status < 400) return RESPONSE.redirect(this.#method, raw_response);
|
|
169
|
+
if (raw_response.status >= 400 && raw_response.status < 600) {
|
|
105
170
|
let error;
|
|
106
171
|
if (this.#parsers.error) {
|
|
107
172
|
const parser = this.#parsers.error;
|
|
108
173
|
let parsed;
|
|
109
|
-
if (typeof parser.parse === "function") parsed = await parser.parse(
|
|
110
|
-
else if (parser.parse === "json") parsed = await parse_as_json(
|
|
111
|
-
else if (parser.parse === "text") parsed = await
|
|
174
|
+
if (typeof parser.parse === "function") parsed = await parser.parse(response.body);
|
|
175
|
+
else if (parser.parse === "json") parsed = await parse_as_json(response);
|
|
176
|
+
else if (parser.parse === "text") parsed = await response.text();
|
|
112
177
|
const result = await parser.schema["~standard"].validate(parsed);
|
|
113
178
|
if (result.issues !== void 0) return new ParseError("Error parsing failed", {
|
|
114
179
|
cause: result.issues,
|
|
115
|
-
operation: "parse_response"
|
|
180
|
+
operation: "parse_response",
|
|
181
|
+
response: {
|
|
182
|
+
status: raw_response.status,
|
|
183
|
+
headers: raw_response.headers,
|
|
184
|
+
body: parsed
|
|
185
|
+
}
|
|
116
186
|
});
|
|
117
187
|
error = result.value;
|
|
118
|
-
} else error = await
|
|
119
|
-
return
|
|
120
|
-
ok: false,
|
|
121
|
-
status,
|
|
122
|
-
error,
|
|
123
|
-
headers,
|
|
124
|
-
raw_response
|
|
125
|
-
};
|
|
188
|
+
} else error = await response.text();
|
|
189
|
+
return raw_response.status >= 400 && raw_response.status < 500 ? RESPONSE.client_error(this.#method, error, raw_response) : RESPONSE.server_error(this.#method, error, raw_response);
|
|
126
190
|
}
|
|
127
|
-
if (status >= 200 && status < 300) {
|
|
128
|
-
if (status === 204) return
|
|
129
|
-
ok: true,
|
|
130
|
-
status: 204,
|
|
131
|
-
data: null,
|
|
132
|
-
headers,
|
|
133
|
-
raw_response
|
|
134
|
-
};
|
|
191
|
+
if (raw_response.status >= 200 && raw_response.status < 300) {
|
|
192
|
+
if (raw_response.status === 204) return RESPONSE.success(this.#method, null, raw_response);
|
|
135
193
|
if (this.#parsers.data) {
|
|
136
194
|
const parser = this.#parsers.data;
|
|
137
195
|
let parsed;
|
|
138
|
-
if (typeof parser.parse === "function") parsed = await parser.parse(
|
|
139
|
-
else if (parser.parse === "json") parsed = await parse_as_json(
|
|
140
|
-
else if (parser.parse === "text") parsed = await
|
|
196
|
+
if (typeof parser.parse === "function") parsed = await parser.parse(response.body);
|
|
197
|
+
else if (parser.parse === "json") parsed = await parse_as_json(response);
|
|
198
|
+
else if (parser.parse === "text") parsed = await response.text();
|
|
141
199
|
const result = await parser.schema["~standard"].validate(parsed);
|
|
142
200
|
if (result.issues !== void 0) return new ParseError("Response parsing failed", {
|
|
143
201
|
cause: result.issues,
|
|
144
|
-
operation: "parse_response"
|
|
202
|
+
operation: "parse_response",
|
|
203
|
+
response: {
|
|
204
|
+
status: raw_response.status,
|
|
205
|
+
headers: raw_response.headers,
|
|
206
|
+
body: parsed
|
|
207
|
+
}
|
|
145
208
|
});
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
status,
|
|
149
|
-
data: result.value,
|
|
150
|
-
headers,
|
|
151
|
-
raw_response
|
|
152
|
-
};
|
|
153
|
-
} else return {
|
|
154
|
-
ok: true,
|
|
155
|
-
status,
|
|
156
|
-
data: null,
|
|
157
|
-
headers,
|
|
158
|
-
raw_response
|
|
159
|
-
};
|
|
209
|
+
return RESPONSE.success(this.#method, result.value, raw_response);
|
|
210
|
+
} else return RESPONSE.success(this.#method, null, raw_response);
|
|
160
211
|
}
|
|
161
212
|
throw new Error(`Unhandled status code: ${status}`);
|
|
162
213
|
}
|
package/dist/lib/errors.d.mts
CHANGED
|
@@ -1,54 +1,78 @@
|
|
|
1
1
|
//#region src/lib/errors.d.ts
|
|
2
|
-
type
|
|
3
|
-
|
|
2
|
+
type RequestContext = {
|
|
3
|
+
url: string;
|
|
4
|
+
method: string;
|
|
5
|
+
pathname?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
headers?: Headers;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
};
|
|
10
|
+
type ResponseContext = {
|
|
11
|
+
status: number;
|
|
12
|
+
statusText?: string;
|
|
13
|
+
headers?: Headers;
|
|
14
|
+
body?: unknown;
|
|
15
|
+
};
|
|
16
|
+
type TimingContext = {
|
|
17
|
+
startTime?: number;
|
|
18
|
+
duration?: number;
|
|
19
|
+
attempt?: number;
|
|
20
|
+
maxAttempts?: number;
|
|
4
21
|
};
|
|
5
|
-
type
|
|
6
|
-
|
|
22
|
+
type InputContext = {
|
|
23
|
+
params?: unknown;
|
|
24
|
+
query?: unknown;
|
|
25
|
+
body?: unknown;
|
|
26
|
+
};
|
|
27
|
+
type ErrorContext = {
|
|
28
|
+
operation: string;
|
|
29
|
+
request?: RequestContext;
|
|
30
|
+
response?: ResponseContext;
|
|
31
|
+
timing?: TimingContext;
|
|
32
|
+
input?: InputContext;
|
|
7
33
|
};
|
|
8
34
|
declare class HttpClientError extends Error {
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
declare class TimeoutError extends HttpClientError {
|
|
12
|
-
readonly context: BaseContext;
|
|
35
|
+
readonly context: ErrorContext;
|
|
13
36
|
constructor(message: string, {
|
|
14
37
|
cause,
|
|
15
|
-
...
|
|
16
|
-
}:
|
|
38
|
+
...options
|
|
39
|
+
}: {
|
|
40
|
+
cause?: unknown;
|
|
41
|
+
} & Partial<ErrorContext>);
|
|
42
|
+
}
|
|
43
|
+
declare class TimeoutError extends HttpClientError {
|
|
44
|
+
constructor(message: string, options: {
|
|
45
|
+
cause?: unknown;
|
|
46
|
+
} & Partial<ErrorContext>);
|
|
17
47
|
}
|
|
18
48
|
declare class AbortedError extends HttpClientError {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
...context
|
|
23
|
-
}: ErrorCause & BaseContext);
|
|
49
|
+
constructor(message: string, options: {
|
|
50
|
+
cause?: unknown;
|
|
51
|
+
} & Partial<ErrorContext>);
|
|
24
52
|
}
|
|
25
53
|
declare class SerializationError extends HttpClientError {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
...context
|
|
30
|
-
}: ErrorCause & BaseContext);
|
|
54
|
+
constructor(message: string, options: {
|
|
55
|
+
cause?: unknown;
|
|
56
|
+
} & Partial<ErrorContext>);
|
|
31
57
|
}
|
|
32
58
|
declare class ParseError extends HttpClientError {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
...context
|
|
37
|
-
}: ErrorCause & BaseContext);
|
|
59
|
+
constructor(message: string, options: {
|
|
60
|
+
cause?: unknown;
|
|
61
|
+
} & Partial<ErrorContext>);
|
|
38
62
|
}
|
|
39
63
|
declare class NetworkError extends HttpClientError {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
...context
|
|
44
|
-
}: ErrorCause & BaseContext);
|
|
64
|
+
constructor(message: string, options: {
|
|
65
|
+
cause?: unknown;
|
|
66
|
+
} & Partial<ErrorContext>);
|
|
45
67
|
}
|
|
46
68
|
declare class UnexpectedError extends Error {
|
|
47
|
-
readonly context:
|
|
69
|
+
readonly context: ErrorContext;
|
|
48
70
|
constructor(message: string, {
|
|
49
71
|
cause,
|
|
50
|
-
...
|
|
51
|
-
}:
|
|
72
|
+
...options
|
|
73
|
+
}: {
|
|
74
|
+
cause?: unknown;
|
|
75
|
+
} & Partial<ErrorContext>);
|
|
52
76
|
}
|
|
53
77
|
//#endregion
|
|
54
78
|
export { AbortedError, HttpClientError, NetworkError, ParseError, SerializationError, TimeoutError, UnexpectedError };
|
package/dist/lib/errors.mjs
CHANGED
|
@@ -1,56 +1,54 @@
|
|
|
1
1
|
//#region src/lib/errors.ts
|
|
2
2
|
var HttpClientError = class extends Error {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
context;
|
|
4
|
+
constructor(message, { cause, ...options }) {
|
|
5
|
+
super(message, { cause });
|
|
5
6
|
this.name = "HttpClientError";
|
|
7
|
+
this.context = {
|
|
8
|
+
...options,
|
|
9
|
+
operation: options.operation ?? "unknown"
|
|
10
|
+
};
|
|
6
11
|
}
|
|
7
12
|
};
|
|
8
13
|
var TimeoutError = class extends HttpClientError {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
super(message, { cause });
|
|
14
|
+
constructor(message, options) {
|
|
15
|
+
super(message, options);
|
|
12
16
|
this.name = "TimeoutError";
|
|
13
|
-
this.context = context;
|
|
14
17
|
}
|
|
15
18
|
};
|
|
16
19
|
var AbortedError = class extends HttpClientError {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
super(message, { cause });
|
|
20
|
+
constructor(message, options) {
|
|
21
|
+
super(message, options);
|
|
20
22
|
this.name = "AbortedError";
|
|
21
|
-
this.context = context;
|
|
22
23
|
}
|
|
23
24
|
};
|
|
24
25
|
var SerializationError = class extends HttpClientError {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
super(message, { cause });
|
|
26
|
+
constructor(message, options) {
|
|
27
|
+
super(message, options);
|
|
28
28
|
this.name = "SerializationError";
|
|
29
|
-
this.context = context;
|
|
30
29
|
}
|
|
31
30
|
};
|
|
32
31
|
var ParseError = class extends HttpClientError {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
super(message, { cause });
|
|
32
|
+
constructor(message, options) {
|
|
33
|
+
super(message, options);
|
|
36
34
|
this.name = "ParseError";
|
|
37
|
-
this.context = context;
|
|
38
35
|
}
|
|
39
36
|
};
|
|
40
37
|
var NetworkError = class extends HttpClientError {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
super(message, { cause });
|
|
38
|
+
constructor(message, options) {
|
|
39
|
+
super(message, options);
|
|
44
40
|
this.name = "NetworkError";
|
|
45
|
-
this.context = context;
|
|
46
41
|
}
|
|
47
42
|
};
|
|
48
43
|
var UnexpectedError = class extends Error {
|
|
49
44
|
context;
|
|
50
|
-
constructor(message, { cause, ...
|
|
45
|
+
constructor(message, { cause, ...options }) {
|
|
51
46
|
super(message, { cause });
|
|
52
47
|
this.name = "UnexpectedError";
|
|
53
|
-
this.context =
|
|
48
|
+
this.context = {
|
|
49
|
+
...options,
|
|
50
|
+
operation: options.operation ?? "unknown"
|
|
51
|
+
};
|
|
54
52
|
}
|
|
55
53
|
};
|
|
56
54
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbortedError, NetworkError, ParseError, SerializationError, TimeoutError, UnexpectedError } from "./errors.mjs";
|
|
2
|
-
import { HTTPFetch, HTTPMethod, HeadersInitWithReducer, MaybePromise, Schema } from "./types.mjs";
|
|
2
|
+
import { HTTPFetch, HTTPMethod, HeadersInitWithReducer, MaybePromise, Pathname, Pretty, Schema } from "./types.mjs";
|
|
3
3
|
import { AnyEndpoint, Endpoint } from "./endpoint.mjs";
|
|
4
4
|
import * as _standard_schema_spec0 from "@standard-schema/spec";
|
|
5
5
|
|
|
@@ -8,6 +8,23 @@ interface EndpointMap {
|
|
|
8
8
|
[name: string]: AnyEndpoint | EndpointMap;
|
|
9
9
|
}
|
|
10
10
|
type CustomFetch = (request: Request) => Promise<Response>;
|
|
11
|
+
type Hooks = {
|
|
12
|
+
on_request?: (request: Request) => void;
|
|
13
|
+
on_response?: (response: Response) => void;
|
|
14
|
+
};
|
|
15
|
+
declare function fetch_endpoint_factory<http_method extends HTTPMethod.Any, pathname extends Pathname.Relative, params_schema extends Schema._, query_schema extends Schema._, body_schema extends Schema._, data_schema extends Schema._, error_schema extends Schema._>({
|
|
16
|
+
base_url,
|
|
17
|
+
endpoint,
|
|
18
|
+
custom_fetch,
|
|
19
|
+
get_default_options,
|
|
20
|
+
hooks
|
|
21
|
+
}: {
|
|
22
|
+
base_url: string;
|
|
23
|
+
endpoint: Endpoint<http_method, pathname, params_schema, query_schema, body_schema, data_schema, error_schema>;
|
|
24
|
+
custom_fetch: CustomFetch;
|
|
25
|
+
get_default_options?: () => MaybePromise<HTTPFetch.OptionalRequestInit & HTTPFetch.DefaultRequestInit>;
|
|
26
|
+
hooks?: Hooks;
|
|
27
|
+
}): (input: Pretty<HTTPFetch.TypedParamsInit<pathname, params_schema> & HTTPFetch.TypedQueryInit<query_schema> & HTTPFetch.TypedBodyInit<body_schema> & HTTPFetch.OptionalRequestInit & HTTPFetch.DefaultRequestInit>) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>>;
|
|
11
28
|
type HttpClientOptions<endpoints extends EndpointMap> = {
|
|
12
29
|
base_url: string;
|
|
13
30
|
endpoints: endpoints;
|
|
@@ -60,5 +77,19 @@ declare function http_client<const endpoints extends EndpointMap>({
|
|
|
60
77
|
} & Omit<RequestInit, "body" | "method" | "headers"> extends infer T_16 ? { [K_10 in keyof T_16]: T_16[K_10] } : never) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>> : T_15[name_9] extends EndpointMap ? (T_15[name_9] extends infer T_17 extends EndpointMap ? { -readonly [name_10 in keyof T_17]: T_17[name_10] extends Endpoint<infer http_method extends HTTPMethod.Any, infer pathname extends `/${string}`, infer params_schema extends Schema._<unknown, unknown>, infer query_schema extends Schema._<unknown, unknown>, infer body_schema extends Schema._<unknown, unknown>, infer data_schema extends Schema._<unknown, unknown>, infer error_schema extends Schema._<unknown, unknown>> ? (input: HTTPFetch.TypedParamsInit<pathname, params_schema> & HTTPFetch.TypedQueryInit<query_schema> & HTTPFetch.TypedBodyInit<body_schema> & HTTPFetch.OptionalRequestInit & {
|
|
61
78
|
headers?: HeadersInitWithReducer;
|
|
62
79
|
} & Omit<RequestInit, "body" | "method" | "headers"> extends infer T_18 ? { [K_11 in keyof T_18]: T_18[K_11] } : never) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>> : T_17[name_10] extends EndpointMap ? (T_17[name_10] extends infer T_19 extends EndpointMap ? { -readonly [name_11 in keyof T_19]: T_19[name_11] extends Endpoint<infer http_method extends HTTPMethod.Any, infer pathname extends `/${string}`, infer params_schema extends Schema._<unknown, unknown>, infer query_schema extends Schema._<unknown, unknown>, infer body_schema extends Schema._<unknown, unknown>, infer data_schema extends Schema._<unknown, unknown>, infer error_schema extends Schema._<unknown, unknown>> ? (input: /*elided*/any) => Promise<TimeoutError | AbortedError | SerializationError | ParseError | NetworkError | UnexpectedError | HTTPFetch.RedirectMessage | HTTPFetch.ClientErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.ServerErrorResponse<[error_schema] extends [never] ? string : _standard_schema_spec0.StandardSchemaV1.InferOutput<error_schema>> | HTTPFetch.SuccessfulResponse<[data_schema] extends [never] ? void : _standard_schema_spec0.StandardSchemaV1.InferOutput<data_schema>>> : T_19[name_11] extends EndpointMap ? /*elided*/any : never } : never) extends infer T_18 ? { [K_11 in keyof T_18]: T_18[K_11] } : never : never } : never) extends infer T_16 ? { [K_10 in keyof T_16]: T_16[K_10] } : never : never } : never) extends infer T_14 ? { [K_9 in keyof T_14]: T_14[K_9] } : never : never } : never) extends infer T_12 ? { [K_8 in keyof T_12]: T_12[K_8] } : never : never } : never) extends infer T_10 ? { [K_7 in keyof T_10]: T_10[K_7] } : never : never } : never) extends infer T_8 ? { [K_6 in keyof T_8]: T_8[K_6] } : never : never } : never) extends infer T_6 ? { [K_5 in keyof T_6]: T_6[K_5] } : never : never } : never) extends infer T_4 ? { [K_4 in keyof T_4]: T_4[K_4] } : never : never } : never) extends infer T_2 ? { [K_3 in keyof T_2]: T_2[K_3] } : never : never } : never) extends infer T ? { [K_2 in keyof T]: T[K_2] } : never : never }[K] };
|
|
80
|
+
type AnyFetchEndpointFunction = ReturnType<typeof fetch_endpoint_factory<any, any, any, any, any, any, any>>;
|
|
81
|
+
declare namespace $infer {
|
|
82
|
+
type Query<fetch_endpoint extends AnyFetchEndpointFunction> = Parameters<fetch_endpoint>[0] extends {
|
|
83
|
+
query: infer query;
|
|
84
|
+
} ? query : never;
|
|
85
|
+
type Params<fetch_endpoint extends AnyFetchEndpointFunction> = Parameters<fetch_endpoint>[0] extends {
|
|
86
|
+
params: infer params;
|
|
87
|
+
} ? params : never;
|
|
88
|
+
type Body<fetch_endpoint extends AnyFetchEndpointFunction> = Parameters<fetch_endpoint>[0] extends {
|
|
89
|
+
body: infer body;
|
|
90
|
+
} ? body : never;
|
|
91
|
+
type Data<fetch_endpoint extends AnyFetchEndpointFunction> = ReturnType<fetch_endpoint> extends HTTPFetch.SuccessfulResponse<infer data> ? data : never;
|
|
92
|
+
type Error<fetch_endpoint extends AnyFetchEndpointFunction> = ReturnType<fetch_endpoint> extends HTTPFetch.ClientErrorResponse<infer error> ? error : ReturnType<fetch_endpoint> extends HTTPFetch.ServerErrorResponse<infer error> ? error : never;
|
|
93
|
+
}
|
|
63
94
|
//#endregion
|
|
64
|
-
export { EndpointMap, HttpClientOptions, http_client };
|
|
95
|
+
export { $infer, EndpointMap, HttpClientOptions, http_client };
|
package/dist/lib/http-client.mjs
CHANGED
|
@@ -6,7 +6,16 @@ import { extract_args, merge_options, remove_custom_options, sleep } from "./uti
|
|
|
6
6
|
//#region src/lib/http-client.ts
|
|
7
7
|
function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_options = () => ({}), hooks = {} }) {
|
|
8
8
|
async function fetch_endpoint(input) {
|
|
9
|
-
|
|
9
|
+
let start_time = Date.now();
|
|
10
|
+
if (!URL.canParse(base_url)) return new UnexpectedError(`Invalid base_url: ${base_url}`, {
|
|
11
|
+
operation: "base_url_validation",
|
|
12
|
+
request: {
|
|
13
|
+
url: base_url,
|
|
14
|
+
method: endpoint.method,
|
|
15
|
+
baseUrl: base_url
|
|
16
|
+
},
|
|
17
|
+
timing: { startTime: start_time }
|
|
18
|
+
});
|
|
10
19
|
const { args, options } = extract_args(input);
|
|
11
20
|
const { headers, ...merged_options } = merge_options(await get_default_options(), endpoint.options, options);
|
|
12
21
|
const url = await endpoint.generate_url({
|
|
@@ -15,12 +24,29 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
|
|
|
15
24
|
query: args.query
|
|
16
25
|
}).catch((error) => new UnexpectedError("Failed to generate URL", {
|
|
17
26
|
cause: error,
|
|
18
|
-
operation: "generate_url"
|
|
27
|
+
operation: "generate_url",
|
|
28
|
+
request: {
|
|
29
|
+
url: base_url,
|
|
30
|
+
method: endpoint.method,
|
|
31
|
+
baseUrl: base_url
|
|
32
|
+
},
|
|
33
|
+
input: {
|
|
34
|
+
params: args.params,
|
|
35
|
+
query: args.query
|
|
36
|
+
},
|
|
37
|
+
timing: { startTime: start_time }
|
|
19
38
|
}));
|
|
20
39
|
if (url instanceof Error) return url;
|
|
21
40
|
const serialized = await endpoint.serialize_body({ body: args.body }).catch((error) => new UnexpectedError("Failed to serialize body", {
|
|
22
41
|
cause: error,
|
|
23
|
-
operation: "serialize_body"
|
|
42
|
+
operation: "serialize_body",
|
|
43
|
+
request: {
|
|
44
|
+
url: url instanceof URL ? url.toString() : base_url,
|
|
45
|
+
method: endpoint.method,
|
|
46
|
+
baseUrl: base_url
|
|
47
|
+
},
|
|
48
|
+
input: { body: args.body },
|
|
49
|
+
timing: { startTime: start_time }
|
|
24
50
|
}));
|
|
25
51
|
if (serialized instanceof Error) return serialized;
|
|
26
52
|
headers.delete("Content-Type");
|
|
@@ -50,7 +76,18 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
|
|
|
50
76
|
} catch (local_error) {
|
|
51
77
|
error = new UnexpectedError("Failed to create request", {
|
|
52
78
|
cause: local_error,
|
|
53
|
-
operation: "create_request"
|
|
79
|
+
operation: "create_request",
|
|
80
|
+
request: {
|
|
81
|
+
url: url instanceof URL ? url.toString() : base_url,
|
|
82
|
+
method: endpoint.method,
|
|
83
|
+
headers,
|
|
84
|
+
timeout: options.timeout,
|
|
85
|
+
baseUrl: base_url
|
|
86
|
+
},
|
|
87
|
+
timing: {
|
|
88
|
+
startTime: start_time,
|
|
89
|
+
attempt: 1
|
|
90
|
+
}
|
|
54
91
|
});
|
|
55
92
|
break;
|
|
56
93
|
}
|
|
@@ -60,17 +97,48 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
|
|
|
60
97
|
response = await custom_fetch(request);
|
|
61
98
|
error = void 0;
|
|
62
99
|
} catch (local_error) {
|
|
100
|
+
const duration = Date.now() - start_time;
|
|
63
101
|
if (local_error instanceof Error && local_error.name === "TimeoutError") error = new TimeoutError(local_error.message, {
|
|
64
102
|
cause: local_error,
|
|
65
|
-
operation: "fetch"
|
|
103
|
+
operation: "fetch",
|
|
104
|
+
request: {
|
|
105
|
+
url: request.url,
|
|
106
|
+
method: request.method,
|
|
107
|
+
timeout: options.timeout
|
|
108
|
+
},
|
|
109
|
+
timing: {
|
|
110
|
+
startTime: start_time,
|
|
111
|
+
duration,
|
|
112
|
+
attempt
|
|
113
|
+
}
|
|
66
114
|
});
|
|
67
115
|
else if (local_error instanceof Error && local_error.name === "AbortError") error = new AbortedError(local_error.message, {
|
|
68
116
|
cause: local_error,
|
|
69
|
-
operation: "fetch"
|
|
117
|
+
operation: "fetch",
|
|
118
|
+
request: {
|
|
119
|
+
url: request.url,
|
|
120
|
+
method: request.method,
|
|
121
|
+
timeout: options.timeout
|
|
122
|
+
},
|
|
123
|
+
timing: {
|
|
124
|
+
startTime: start_time,
|
|
125
|
+
duration,
|
|
126
|
+
attempt
|
|
127
|
+
}
|
|
70
128
|
});
|
|
71
129
|
else error = new NetworkError("Network error", {
|
|
72
130
|
cause: local_error,
|
|
73
|
-
operation: "fetch"
|
|
131
|
+
operation: "fetch",
|
|
132
|
+
request: {
|
|
133
|
+
url: request.url,
|
|
134
|
+
method: request.method,
|
|
135
|
+
timeout: options.timeout
|
|
136
|
+
},
|
|
137
|
+
timing: {
|
|
138
|
+
startTime: start_time,
|
|
139
|
+
duration,
|
|
140
|
+
attempt
|
|
141
|
+
}
|
|
74
142
|
});
|
|
75
143
|
}
|
|
76
144
|
try {
|
|
@@ -91,25 +159,77 @@ function fetch_endpoint_factory({ base_url, endpoint, custom_fetch, get_default_
|
|
|
91
159
|
} catch (local_error) {
|
|
92
160
|
error = new UnexpectedError("Failed to check retry policy", {
|
|
93
161
|
cause: local_error,
|
|
94
|
-
operation: "retry_policy"
|
|
162
|
+
operation: "retry_policy",
|
|
163
|
+
request: {
|
|
164
|
+
url: url instanceof URL ? url.toString() : base_url,
|
|
165
|
+
method: endpoint.method,
|
|
166
|
+
timeout: options.timeout,
|
|
167
|
+
baseUrl: base_url
|
|
168
|
+
},
|
|
169
|
+
timing: {
|
|
170
|
+
startTime: start_time,
|
|
171
|
+
attempt,
|
|
172
|
+
maxAttempts: typeof retry_policy.attempts === "function" ? void 0 : retry_policy.attempts
|
|
173
|
+
}
|
|
95
174
|
});
|
|
96
175
|
break;
|
|
97
176
|
}
|
|
98
177
|
} while (true);
|
|
99
178
|
if (error) return error;
|
|
100
|
-
if (!response) return new UnexpectedError("", {
|
|
179
|
+
if (!response) return new UnexpectedError("No response received", {
|
|
101
180
|
cause: "No response received",
|
|
102
|
-
operation: "parse_response"
|
|
181
|
+
operation: "parse_response",
|
|
182
|
+
request: {
|
|
183
|
+
url: url instanceof URL ? url.toString() : base_url,
|
|
184
|
+
method: endpoint.method,
|
|
185
|
+
timeout: options.timeout,
|
|
186
|
+
baseUrl: base_url
|
|
187
|
+
},
|
|
188
|
+
timing: {
|
|
189
|
+
startTime: start_time,
|
|
190
|
+
attempt
|
|
191
|
+
}
|
|
103
192
|
});
|
|
104
193
|
hooks.on_response?.(response);
|
|
105
|
-
return await endpoint.parse_response(response).catch((error) => {
|
|
194
|
+
return await endpoint.parse_response(response).catch(async (error) => {
|
|
195
|
+
const response_body = await response.clone().text().catch(() => void 0);
|
|
106
196
|
if (error instanceof Error && error.name === "AbortError") return new AbortedError(error.message, {
|
|
107
197
|
cause: error,
|
|
108
|
-
operation: "parse_response"
|
|
198
|
+
operation: "parse_response",
|
|
199
|
+
request: {
|
|
200
|
+
url: response.url,
|
|
201
|
+
method: request?.method
|
|
202
|
+
},
|
|
203
|
+
response: {
|
|
204
|
+
status: response.status,
|
|
205
|
+
statusText: response.statusText,
|
|
206
|
+
headers: response.headers,
|
|
207
|
+
body: response_body
|
|
208
|
+
},
|
|
209
|
+
timing: {
|
|
210
|
+
startTime: start_time,
|
|
211
|
+
attempt
|
|
212
|
+
}
|
|
109
213
|
});
|
|
110
214
|
return new UnexpectedError("Failed to parse response", {
|
|
111
215
|
cause: error,
|
|
112
|
-
operation: "parse_response"
|
|
216
|
+
operation: "parse_response",
|
|
217
|
+
request: {
|
|
218
|
+
url: response.url,
|
|
219
|
+
method: request?.method,
|
|
220
|
+
timeout: options.timeout,
|
|
221
|
+
baseUrl: base_url
|
|
222
|
+
},
|
|
223
|
+
response: {
|
|
224
|
+
status: response.status,
|
|
225
|
+
statusText: response.statusText,
|
|
226
|
+
headers: response.headers,
|
|
227
|
+
body: response_body
|
|
228
|
+
},
|
|
229
|
+
timing: {
|
|
230
|
+
startTime: start_time,
|
|
231
|
+
attempt
|
|
232
|
+
}
|
|
113
233
|
});
|
|
114
234
|
});
|
|
115
235
|
}
|
package/dist/lib/types.d.mts
CHANGED
package/docs/http-client.md
CHANGED
|
@@ -145,3 +145,46 @@ if (result.ok) {
|
|
|
145
145
|
console.log(result.error)
|
|
146
146
|
}
|
|
147
147
|
```
|
|
148
|
+
|
|
149
|
+
## Type Inference
|
|
150
|
+
|
|
151
|
+
The `$infer` namespace provides helpers to extract types from endpoint functions:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { $infer, http_client, Endpoint } from '@afoures/http-client'
|
|
155
|
+
import { z } from 'zod'
|
|
156
|
+
|
|
157
|
+
const api = http_client({
|
|
158
|
+
base_url: 'https://api.example.com',
|
|
159
|
+
endpoints: {
|
|
160
|
+
users: {
|
|
161
|
+
list: new Endpoint({
|
|
162
|
+
method: 'GET',
|
|
163
|
+
pathname: '/users',
|
|
164
|
+
data: { schema: z.array(z.object({ id: z.string(), name: z.string() })), parse: 'json' },
|
|
165
|
+
}),
|
|
166
|
+
create: new Endpoint({
|
|
167
|
+
method: 'POST',
|
|
168
|
+
pathname: '/users',
|
|
169
|
+
body: { schema: z.object({ name: z.string() }) },
|
|
170
|
+
data: { schema: z.object({ id: z.string(), name: z.string() }), parse: 'json' },
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
type UsersListQuery = $infer.Query<typeof api.users.list>
|
|
177
|
+
type UsersListData = $infer.Data<typeof api.users.list>
|
|
178
|
+
type UsersListParams = $infer.Params<typeof api.users.list>
|
|
179
|
+
type UsersListError = $infer.Error<typeof api.users.list>
|
|
180
|
+
|
|
181
|
+
type UsersCreateBody = $infer.Body<typeof api.users.create>
|
|
182
|
+
type UsersCreateData = $infer.Data<typeof api.users.create>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Available type helpers:
|
|
186
|
+
- `$infer.Query` - Extracts the query parameter type
|
|
187
|
+
- `$infer.Params` - Extracts the URL params type
|
|
188
|
+
- `$infer.Body` - Extracts the request body type
|
|
189
|
+
- `$infer.Data` - Extracts the successful response data type
|
|
190
|
+
- `$infer.Error` - Extracts the error response type (client or server errors)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afoures/http-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A typesafe and robust HTTP client",
|
|
5
5
|
"homepage": "https://github.com/afoures/http-client#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"@standard-schema/spec": "^1.1.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@afoures/auto-release": "^0.
|
|
38
|
+
"@afoures/auto-release": "^0.5.0",
|
|
39
39
|
"@arktype/attest": "^0.56.0",
|
|
40
|
-
"@types/node": "^24.12.
|
|
41
|
-
"msw": "^2.
|
|
40
|
+
"@types/node": "^24.12.2",
|
|
41
|
+
"msw": "^2.13.2",
|
|
42
42
|
"oxfmt": "^0.36.0",
|
|
43
|
-
"oxlint": "^1.
|
|
43
|
+
"oxlint": "^1.59.0",
|
|
44
44
|
"tsdown": "^0.21.0",
|
|
45
45
|
"typescript": "^5.9.3",
|
|
46
46
|
"zod": "^4.3.6"
|