@nestia/fetcher 14.0.1 → 14.0.2
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/lib/AesPkcs5.d.ts +19 -2
- package/lib/AesPkcs5.js +21 -4
- package/lib/AesPkcs5.js.map +1 -1
- package/lib/AesPkcs5.mjs +2 -2
- package/lib/AesPkcs5.mjs.map +1 -1
- package/lib/EncryptedFetcher.d.ts +34 -3
- package/lib/EncryptedFetcher.js +44 -54
- package/lib/EncryptedFetcher.js.map +1 -1
- package/lib/EncryptedFetcher.mjs +30 -25
- package/lib/EncryptedFetcher.mjs.map +1 -1
- package/lib/FormDataInput.d.ts +21 -8
- package/lib/IConnection.d.ts +49 -14
- package/lib/IEncryptionPassword.d.ts +18 -2
- package/lib/IFetchEvent.d.ts +25 -0
- package/lib/IFetchEvent.js +0 -18
- package/lib/IFetchEvent.js.map +1 -1
- package/lib/IFetchRoute.d.ts +19 -0
- package/lib/IPropagation.d.ts +32 -10
- package/lib/NestiaSimulator.d.ts +37 -0
- package/lib/NestiaSimulator.js +62 -13
- package/lib/NestiaSimulator.js.map +1 -1
- package/lib/NestiaSimulator.mjs +36 -8
- package/lib/NestiaSimulator.mjs.map +1 -1
- package/lib/PathParameter.d.ts +8 -0
- package/lib/PathParameter.js +8 -0
- package/lib/PathParameter.js.map +1 -1
- package/lib/PathParameter.mjs.map +1 -1
- package/lib/PlainFetcher.d.ts +36 -5
- package/lib/PlainFetcher.js +6 -2
- package/lib/PlainFetcher.js.map +1 -1
- package/lib/PlainFetcher.mjs.map +1 -1
- package/lib/internal/FetcherBase.js +31 -7
- package/lib/internal/FetcherBase.js.map +1 -1
- package/lib/internal/FetcherBase.mjs +11 -5
- package/lib/internal/FetcherBase.mjs.map +1 -1
- package/lib/internal/is_binary_response_content_type.d.ts +12 -0
- package/lib/internal/is_binary_response_content_type.js +12 -0
- package/lib/internal/is_binary_response_content_type.js.map +1 -1
- package/lib/internal/is_binary_response_content_type.mjs +12 -0
- package/lib/internal/is_binary_response_content_type.mjs.map +1 -1
- package/package.json +4 -3
- package/src/AesPkcs5.ts +21 -4
- package/src/EncryptedFetcher.ts +77 -57
- package/src/FormDataInput.ts +26 -10
- package/src/IConnection.ts +54 -18
- package/src/IEncryptionPassword.ts +18 -2
- package/src/IFetchEvent.ts +32 -19
- package/src/IFetchRoute.ts +19 -0
- package/src/IPropagation.ts +32 -10
- package/src/NestiaSimulator.ts +66 -14
- package/src/PathParameter.ts +8 -0
- package/src/PlainFetcher.ts +50 -5
- package/src/internal/FetcherBase.ts +44 -6
- package/src/internal/is_binary_response_content_type.ts +12 -0
package/src/IPropagation.ts
CHANGED
|
@@ -7,20 +7,21 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ```typescript
|
|
9
9
|
* type Output = IPropagation<{
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 200: ISeller.IAuthorized;
|
|
11
|
+
* 400: TypeGuardError.IProps;
|
|
12
|
+
* }>;
|
|
13
13
|
*
|
|
14
14
|
* const output: Output = await sdk.sellers.authenticate.join(input);
|
|
15
15
|
* if (output.success) {
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* // automatically casted to "ISeller.IAuthorized" type
|
|
17
|
+
* const authorized: ISeller.IAuthorized = output.data;
|
|
18
18
|
* } else if (output.status === 400) {
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* // The unknown-status fallback overlaps numeric statuses. Validate the
|
|
20
|
+
* // body before treating it as TypeGuardError.IProps.
|
|
21
|
+
* const error: unknown = output.data;
|
|
21
22
|
* } else {
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* // unknown type when out of pre-defined status codes
|
|
24
|
+
* const result: unknown = output.data;
|
|
24
25
|
* }
|
|
25
26
|
* ```
|
|
26
27
|
*
|
|
@@ -33,6 +34,10 @@
|
|
|
33
34
|
* @author Jeongho Nam - https://github.com/samchon
|
|
34
35
|
* @template StatusMap Map of status code and its body data type.
|
|
35
36
|
* @template Success Default success status code.
|
|
37
|
+
* @evidence contracts/common.md#principled-implementation Each configured status becomes a branch whose success flag is true for configured success statuses, 200 and 201 by default. A false-success fallback with numeric status and unknown data represents unconfigured responses; TypeScript cannot subtract numeric literals from number, so this fallback also overlaps configured failure statuses and status-only narrowing cannot prove their payload shape.
|
|
38
|
+
* @evidence contracts/common.md#clear-and-simple-design One mapped type indexed into a union, plus the fallback branch.
|
|
39
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
|
|
40
|
+
* @evidence contracts/common.md#meaningful-documentation The comment gives an example of narrowing and says which generated SDK option uses the type.
|
|
36
41
|
*/
|
|
37
42
|
export type IPropagation<
|
|
38
43
|
StatusMap extends {
|
|
@@ -55,6 +60,11 @@ export namespace IPropagation {
|
|
|
55
60
|
* The special characters like `2XX`, `3XX`, `4XX`, `5XX` are meaning the
|
|
56
61
|
* range of status codes. If `5XX` is specified, it means the status code is
|
|
57
62
|
* in the range of `500` to `599`.
|
|
63
|
+
*
|
|
64
|
+
* @evidence contracts/common.md#principled-implementation A status is a number or one of the four range spellings, which is enough to configure both exact and ranged branches.
|
|
65
|
+
* @evidence contracts/common.md#clear-and-simple-design A single union.
|
|
66
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
|
|
67
|
+
* @evidence contracts/common.md#meaningful-documentation The comment defines the range spellings.
|
|
58
68
|
*/
|
|
59
69
|
export type Status = number | "2XX" | "3XX" | "4XX" | "5XX";
|
|
60
70
|
|
|
@@ -64,6 +74,11 @@ export namespace IPropagation {
|
|
|
64
74
|
* `IPropagation.IBranch` is a branch type composing `IPropagation` type,
|
|
65
75
|
* which is gathering all possible status codes and their body data types as a
|
|
66
76
|
* union type.
|
|
77
|
+
*
|
|
78
|
+
* @evidence contracts/common.md#principled-implementation A branch pairs a literal success flag with the status, the data, and the headers, and turns a range spelling into the union of its integers, so a narrowed status is a number the caller can compare.
|
|
79
|
+
* @evidence contracts/common.md#clear-and-simple-design One record with a conditional on the status member.
|
|
80
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
|
|
81
|
+
* @evidence contracts/common.md#meaningful-documentation The comment explains that it is the element of the propagation union.
|
|
67
82
|
*/
|
|
68
83
|
export interface IBranch<Success extends boolean, StatusValue, BodyData> {
|
|
69
84
|
success: Success;
|
|
@@ -76,7 +91,14 @@ export namespace IPropagation {
|
|
|
76
91
|
headers: Record<string, string | string[]>;
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Range of status codes by the first digit, `"4XX"` for `400` to `499`.
|
|
96
|
+
*
|
|
97
|
+
* @evidence contracts/common.md#principled-implementation The first-digit spelling maps to the integers from its hundred up to the next hundred, computed by excluding a smaller enumeration from a larger one.
|
|
98
|
+
* @evidence contracts/common.md#clear-and-simple-design One conditional over the four spellings, using two private helper types.
|
|
99
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
|
|
100
|
+
* @evidence contracts/common.md#meaningful-documentation The comment gives the range rule with an example.
|
|
101
|
+
*/
|
|
80
102
|
export type StatusRange<T extends "2XX" | "3XX" | "4XX" | "5XX"> =
|
|
81
103
|
T extends "2XX"
|
|
82
104
|
? IntRange<200, 300>
|
package/src/NestiaSimulator.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { HttpError } from "./HttpError";
|
|
2
2
|
import { join_host_and_path } from "./internal/join_host_and_path";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Request validation for the mockup simulator of a generated SDK.
|
|
6
|
+
*
|
|
7
|
+
* A simulated function does not call a server. It validates its input with
|
|
8
|
+
* typia and, when the input is wrong, throws the `HttpError` with status 400
|
|
9
|
+
* that the real server would answer with.
|
|
10
|
+
*
|
|
11
|
+
* @evidence contracts/common.md#principled-implementation Each validator runs the caller's assertion and converts a readable typia `TypeGuardError` shape into an `HttpError` 400 whose JSON body carries the method, path, expected type and value plus a message naming the failing part. A shared snapshot reads each needed property once; malformed or unreadable shapes rethrow the caller's original value. Payload values must support JSON serialization, whose errors still propagate.
|
|
12
|
+
* @evidence contracts/common.md#clear-and-simple-design One public entry point, `assert`, returns four validators that differ only in their message, sharing one private conversion; the shape snapshot reader and the error interface are module-private.
|
|
13
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Classification checks the readable error structure (method, path, expected type, name, message and stack) rather than its class, then captures the payload value. A value that lacks any of those fields with the expected types, or whose property access throws, is not classified as a type guard failure and is rethrown unchanged; unrelated task errors are neither replaced nor swallowed.
|
|
14
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what the simulator validates and what it throws.
|
|
15
|
+
*/
|
|
4
16
|
export namespace NestiaSimulator {
|
|
17
|
+
/**
|
|
18
|
+
* Route facts the simulator needs to build its 400 error: the host, the path,
|
|
19
|
+
* and the method.
|
|
20
|
+
*
|
|
21
|
+
* `contentType` is the content type of the route's success response, which
|
|
22
|
+
* the generated code answers with; the simulated 400 is JSON whatever it is.
|
|
23
|
+
*
|
|
24
|
+
* @evidence contracts/common.md#principled-implementation The host, path, and method identify the request in the error, and the content type describes the success response of the generated function, which the generated code answers with.
|
|
25
|
+
* @evidence contracts/common.md#clear-and-simple-design A four-field record with no behavior.
|
|
26
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The values are supplied by the generated SDK for its own route.
|
|
27
|
+
* @evidence contracts/common.md#meaningful-documentation The comment says what each part is used for, with the `contentType` member documenting when it is `null`.
|
|
28
|
+
*/
|
|
5
29
|
export interface IProps {
|
|
6
30
|
host: string;
|
|
7
31
|
path: string;
|
|
@@ -15,6 +39,19 @@ export namespace NestiaSimulator {
|
|
|
15
39
|
contentType: string | null;
|
|
16
40
|
}
|
|
17
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Creates the validators of one simulated route: `param(name)`, `query`,
|
|
44
|
+
* `body`, and `headers`.
|
|
45
|
+
*
|
|
46
|
+
* Each validator takes the closure that performs the typia assertion. A
|
|
47
|
+
* failed assertion throws an `HttpError` with status 400 and a JSON body; any
|
|
48
|
+
* other error is rethrown as it is.
|
|
49
|
+
*
|
|
50
|
+
* @evidence contracts/common.md#principled-implementation The four validators share the conversion in `validate` and differ in the message they attach, which names the URL parameter or states that the query, body, or headers do not follow the promised type.
|
|
51
|
+
* @evidence contracts/common.md#clear-and-simple-design One function returning the four validators as an object, so a generated simulator has one import.
|
|
52
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The HTTP error is the one the real server returns; nothing is simulated beyond running the assertion.
|
|
53
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what it returns and what each validator throws.
|
|
54
|
+
*/
|
|
18
55
|
export const assert = (props: IProps) => {
|
|
19
56
|
return {
|
|
20
57
|
param: param(props),
|
|
@@ -54,13 +91,14 @@ export namespace NestiaSimulator {
|
|
|
54
91
|
)(task);
|
|
55
92
|
|
|
56
93
|
const validate =
|
|
57
|
-
(message: (exp: TypeGuardError) => string
|
|
94
|
+
(message: (exp: TypeGuardError) => string) =>
|
|
58
95
|
(props: IProps) =>
|
|
59
96
|
<T>(task: () => T): void => {
|
|
60
97
|
try {
|
|
61
98
|
task();
|
|
62
99
|
} catch (exp) {
|
|
63
|
-
|
|
100
|
+
const guard: TypeGuardError | null = readTypeGuardError(exp);
|
|
101
|
+
if (guard !== null)
|
|
64
102
|
throw new HttpError(
|
|
65
103
|
props.method,
|
|
66
104
|
join_host_and_path(props.host, props.path),
|
|
@@ -69,11 +107,11 @@ export namespace NestiaSimulator {
|
|
|
69
107
|
"Content-Type": "application/json",
|
|
70
108
|
},
|
|
71
109
|
JSON.stringify({
|
|
72
|
-
method:
|
|
73
|
-
path:
|
|
74
|
-
expected:
|
|
75
|
-
value:
|
|
76
|
-
message: message(
|
|
110
|
+
method: guard.method,
|
|
111
|
+
path: guard.path,
|
|
112
|
+
expected: guard.expected,
|
|
113
|
+
value: guard.value,
|
|
114
|
+
message: message(guard),
|
|
77
115
|
}),
|
|
78
116
|
);
|
|
79
117
|
throw exp;
|
|
@@ -81,13 +119,27 @@ export namespace NestiaSimulator {
|
|
|
81
119
|
};
|
|
82
120
|
}
|
|
83
121
|
|
|
84
|
-
const
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
const readTypeGuardError = (input: any): TypeGuardError | null => {
|
|
123
|
+
if (typeof input !== "object" || input === null) return null;
|
|
124
|
+
try {
|
|
125
|
+
const method: unknown = input.method;
|
|
126
|
+
if (typeof method !== "string") return null;
|
|
127
|
+
const path: unknown = input.path;
|
|
128
|
+
if (path !== undefined && typeof path !== "string") return null;
|
|
129
|
+
const expected: unknown = input.expected;
|
|
130
|
+
if (typeof expected !== "string") return null;
|
|
131
|
+
const name: unknown = input.name;
|
|
132
|
+
if (typeof name !== "string") return null;
|
|
133
|
+
const message: unknown = input.message;
|
|
134
|
+
if (typeof message !== "string") return null;
|
|
135
|
+
const stack: unknown = input.stack;
|
|
136
|
+
if (stack !== undefined && typeof stack !== "string") return null;
|
|
137
|
+
const value: unknown = input.value;
|
|
138
|
+
return { method, path, expected, name, message, stack, value };
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
91
143
|
|
|
92
144
|
interface TypeGuardError extends Error {
|
|
93
145
|
method: string;
|
package/src/PathParameter.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* The path parameters of a generated SDK function, as its URL carries them.
|
|
3
3
|
*
|
|
4
4
|
* @author Jeongho Nam - https://github.com/samchon
|
|
5
|
+
* @evidence contracts/common.md#principled-implementation A path parameter is one URL path segment, so the value must be encoded as one segment and must never spell a dot segment, which the URL parser would resolve away.
|
|
6
|
+
* @evidence contracts/common.md#clear-and-simple-design One namespace with one function, kept apart so both the SDK generator's output and hand-written callers use the same rule.
|
|
7
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The rule applies to every value; no parameter name is special-cased.
|
|
8
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the purpose of the namespace and the function documents the dot-segment refusal.
|
|
5
9
|
*/
|
|
6
10
|
export namespace PathParameter {
|
|
7
11
|
/**
|
|
@@ -18,6 +22,10 @@ export namespace PathParameter {
|
|
|
18
22
|
* @param value Value of the parameter
|
|
19
23
|
* @returns The URI-encoded segment
|
|
20
24
|
* @throws Error when the value is `.` or `..`
|
|
25
|
+
* @evidence contracts/common.md#principled-implementation encodeURIComponent produces a single segment for well-formed Unicode text and throws URIError for unpaired surrogates; nullish input becomes the text null. The encoded text . or .. is refused because the WHATWG URL parser treats it and its percent-encoded spelling as a dot segment.
|
|
26
|
+
* @evidence contracts/common.md#clear-and-simple-design One conversion and one guard, with the reason for the guard stated in the comment.
|
|
27
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The guard prevents a request from silently reaching a different route, so it is a correctness check rather than a workaround for a test.
|
|
28
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the encoding, the nullish rule, the refused segments, their reason, and the thrown error.
|
|
21
29
|
*/
|
|
22
30
|
export const encode = (name: string, value: unknown): string => {
|
|
23
31
|
const encoded: string = encodeURIComponent(
|
package/src/PlainFetcher.ts
CHANGED
|
@@ -8,15 +8,19 @@ import { FetcherBase } from "./internal/FetcherBase";
|
|
|
8
8
|
*
|
|
9
9
|
* `PlainFetcher` is a utility class designed for SDK functions generated by
|
|
10
10
|
* [`@nestia/sdk`](https://nestia.io/docs/sdk/sdk), interacting with the remote
|
|
11
|
-
* HTTP
|
|
11
|
+
* HTTP server API. In other words, this is a collection of dedicated `fetch()`
|
|
12
12
|
* functions for `@nestia/sdk`.
|
|
13
13
|
*
|
|
14
14
|
* For reference, `PlainFetcher` class does not encrypt or decrypt the body data
|
|
15
15
|
* at all. It just delivers plain data without any post processing. If you've
|
|
16
16
|
* defined a controller method through `@EncryptedRoute` or `@EncryptedBody`
|
|
17
|
-
* decorator, then {@
|
|
17
|
+
* decorator, then {@link EncryptedFetcher} class would be used instead.
|
|
18
18
|
*
|
|
19
19
|
* @author Jeongho Nam - https://github.com/samchon
|
|
20
|
+
* @evidence contracts/common.md#principled-implementation The codecs preserve transformed bodies, while FetcherBase owns JSON, form and binary handling. Encrypted metadata is refused before transport. The shared pipeline removes caller Content-Type case-insensitively so multipart boundaries remain fetch-owned, normalizes only host/path separator boundaries, and classifies 200, 201 and the route's configured success status.
|
|
21
|
+
* @evidence contracts/common.md#clear-and-simple-design Two public operations that mirror `EncryptedFetcher`, with the encryption guard as their only own logic.
|
|
22
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The refusal is the contract with the generated SDK, which chooses `EncryptedFetcher` for encrypted routes; no route or header is special-cased.
|
|
23
|
+
* @evidence contracts/common.md#meaningful-documentation The namespace prose says when the generated SDK uses this fetcher and that it does no encryption.
|
|
20
24
|
*/
|
|
21
25
|
export namespace PlainFetcher {
|
|
22
26
|
/**
|
|
@@ -25,6 +29,10 @@ export namespace PlainFetcher {
|
|
|
25
29
|
* @param connection Connection information for the remote HTTP server
|
|
26
30
|
* @param route Route information about the target API
|
|
27
31
|
* @returns Nothing because of `HEAD` method
|
|
32
|
+
* @evidence contracts/common.md#principled-implementation The overloads narrow arguments by method; the implementation refuses encryption metadata and delegates to FetcherBase.request. Failed statuses become HttpError with their original text, successful HEAD has no data, and binary successes transfer response.body unchanged or create a synchronously closed empty stream if absent. Stream consumption and cancellation belong to the caller.
|
|
33
|
+
* @evidence contracts/common.md#clear-and-simple-design One guard and one delegation, with the overloads only splitting the types.
|
|
34
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The guard follows the route metadata, not a path or a connection.
|
|
35
|
+
* @evidence contracts/common.md#meaningful-documentation Each overload documents its parameters and return value.
|
|
28
36
|
*/
|
|
29
37
|
export function fetch(
|
|
30
38
|
connection: IConnection,
|
|
@@ -74,19 +82,56 @@ export namespace PlainFetcher {
|
|
|
74
82
|
})(connection, route, input, stringify);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Fetch function that returns every response as an {@link IPropagation}.
|
|
87
|
+
*
|
|
88
|
+
* An HTTP failure status is returned as a failed branch instead of being
|
|
89
|
+
* thrown. A route that declares an encrypted body and a transport failure
|
|
90
|
+
* still throw. The output must describe numeric status branches with boolean
|
|
91
|
+
* success and string or string-array headers; its data type remains
|
|
92
|
+
* caller-owned.
|
|
93
|
+
*
|
|
94
|
+
* @evidence contracts/common.md#principled-implementation The implementation refuses an encrypted route and delegates to `FetcherBase.propagate`. Its structural branch constraint accepts numeric literal successes, numeric range failures and unknown-status fallback without instantiating a status map with any, while requiring boolean success and actual numeric status/header shapes.
|
|
95
|
+
* @evidence contracts/common.md#clear-and-simple-design One guard and one delegation, with the overloads only splitting the types.
|
|
96
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The function adds no status-specific branch: the success flag comes from the status rules in `FetcherBase`.
|
|
97
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what the returned union carries and which failures still throw.
|
|
98
|
+
*/
|
|
99
|
+
export function propagate<
|
|
100
|
+
Output extends {
|
|
101
|
+
success: boolean;
|
|
102
|
+
status: number;
|
|
103
|
+
headers: Record<string, string | string[]>;
|
|
104
|
+
data: unknown;
|
|
105
|
+
},
|
|
106
|
+
>(
|
|
78
107
|
connection: IConnection,
|
|
79
108
|
route: IFetchRoute<"GET" | "HEAD">,
|
|
80
109
|
): Promise<Output>;
|
|
81
110
|
|
|
82
|
-
export function propagate<
|
|
111
|
+
export function propagate<
|
|
112
|
+
Input,
|
|
113
|
+
Output extends {
|
|
114
|
+
success: boolean;
|
|
115
|
+
status: number;
|
|
116
|
+
headers: Record<string, string | string[]>;
|
|
117
|
+
data: unknown;
|
|
118
|
+
},
|
|
119
|
+
>(
|
|
83
120
|
connection: IConnection,
|
|
84
121
|
route: IFetchRoute<"DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT">,
|
|
85
122
|
input?: Input,
|
|
86
123
|
stringify?: (input: Input) => string,
|
|
87
124
|
): Promise<Output>;
|
|
88
125
|
|
|
89
|
-
export async function propagate<
|
|
126
|
+
export async function propagate<
|
|
127
|
+
Input,
|
|
128
|
+
Output extends {
|
|
129
|
+
success: boolean;
|
|
130
|
+
status: number;
|
|
131
|
+
headers: Record<string, string | string[]>;
|
|
132
|
+
data: unknown;
|
|
133
|
+
},
|
|
134
|
+
>(
|
|
90
135
|
connection: IConnection,
|
|
91
136
|
route: IFetchRoute<"DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT">,
|
|
92
137
|
input?: Input,
|
|
@@ -6,20 +6,41 @@ import { IPropagation } from "../IPropagation";
|
|
|
6
6
|
import { is_binary_response_content_type } from "./is_binary_response_content_type";
|
|
7
7
|
import { join_host_and_path, normalize_route_path } from "./join_host_and_path";
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Shared transport pipeline with injected body codecs.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
10
14
|
export namespace FetcherBase {
|
|
15
|
+
/**
|
|
16
|
+
* Body transformations supplied by the plain or encrypted fetcher.
|
|
17
|
+
*
|
|
18
|
+
* The encode input has already undergone the route's body transformation.
|
|
19
|
+
* Header records describe the corresponding request or response.
|
|
20
|
+
*/
|
|
11
21
|
export interface IProps {
|
|
22
|
+
/** Fetcher name used in request configuration errors. */
|
|
12
23
|
className: string;
|
|
24
|
+
|
|
25
|
+
/** Convert a transformed request body into its wire representation. */
|
|
13
26
|
encode: (
|
|
14
27
|
input: any,
|
|
15
28
|
headers: Record<string, IConnection.HeaderValue | undefined>,
|
|
16
29
|
) => string;
|
|
30
|
+
|
|
31
|
+
/** Decode a textual response not handled by a structured media branch. */
|
|
17
32
|
decode: (
|
|
18
33
|
input: string,
|
|
19
34
|
headers: Record<string, IConnection.HeaderValue | undefined>,
|
|
20
35
|
) => any;
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Return successful response data and throw HttpError for a failed status.
|
|
40
|
+
*
|
|
41
|
+
* Binary response streams transfer to the caller for reading or cancellation;
|
|
42
|
+
* an absent binary body is represented by a closed empty stream.
|
|
43
|
+
*/
|
|
23
44
|
export const request =
|
|
24
45
|
(props: IProps) =>
|
|
25
46
|
async <Input, Output>(
|
|
@@ -45,6 +66,13 @@ export namespace FetcherBase {
|
|
|
45
66
|
return result.data as Output;
|
|
46
67
|
};
|
|
47
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Return the classified HTTP response, including failed status payloads.
|
|
71
|
+
*
|
|
72
|
+
* Failed JSON responses are parsed when readable; malformed JSON remains
|
|
73
|
+
* text. Transport and successful-body decoding errors still reject. Binary
|
|
74
|
+
* response streams transfer to the caller for consumption or cancellation.
|
|
75
|
+
*/
|
|
48
76
|
export const propagate =
|
|
49
77
|
(props: IProps) =>
|
|
50
78
|
async <Input>(
|
|
@@ -55,7 +83,15 @@ export namespace FetcherBase {
|
|
|
55
83
|
): Promise<IPropagation<any, any>> =>
|
|
56
84
|
_Propagate("propagate")(props)(connection, route, input, stringify);
|
|
57
85
|
|
|
58
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Executes one request and transfers binary stream ownership to its caller.
|
|
88
|
+
*
|
|
89
|
+
* A null successful binary body has no producer or bytes, so its replacement
|
|
90
|
+
* stream must start closed. Actual response streams retain their identity;
|
|
91
|
+
* reading and cancellation remain the caller's responsibility.
|
|
92
|
+
*
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
59
95
|
const _Propagate =
|
|
60
96
|
(method: string) =>
|
|
61
97
|
(props: IProps) =>
|
|
@@ -176,14 +212,16 @@ export namespace FetcherBase {
|
|
|
176
212
|
);
|
|
177
213
|
result.data = route.parseQuery ? route.parseQuery(query) : query;
|
|
178
214
|
} else if (is_binary_response_content_type(route.response?.type))
|
|
179
|
-
result.data =
|
|
215
|
+
result.data =
|
|
216
|
+
response.body ??
|
|
217
|
+
new ReadableStream<Uint8Array>({
|
|
218
|
+
start: (controller) => controller.close(),
|
|
219
|
+
});
|
|
180
220
|
else
|
|
181
221
|
result.data = props.decode(await response.text(), result.headers);
|
|
182
222
|
}
|
|
183
223
|
event.output = result.data;
|
|
184
224
|
return result;
|
|
185
|
-
} catch (exp) {
|
|
186
|
-
throw exp;
|
|
187
225
|
} finally {
|
|
188
226
|
event.completed_at = new Date();
|
|
189
227
|
if (connection.logger)
|
|
@@ -215,7 +253,7 @@ const request_form_data_body = (input: Record<string, any>): FormData => {
|
|
|
215
253
|
else encoded.append(key, value);
|
|
216
254
|
};
|
|
217
255
|
for (const [key, value] of Object.entries(input))
|
|
218
|
-
if (Array.isArray(value)) value.
|
|
256
|
+
if (Array.isArray(value)) value.forEach(append(key));
|
|
219
257
|
else append(key)(value);
|
|
220
258
|
return encoded;
|
|
221
259
|
};
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports whether a response content type carries a binary body.
|
|
3
|
+
*
|
|
4
|
+
* The parameters of the type (`; charset=...`) and its case are ignored. Image,
|
|
5
|
+
* video, and audio types, `application/octet-stream`, and `application/pdf` are
|
|
6
|
+
* binary, and the fetcher returns their body as a stream instead of text.
|
|
7
|
+
*
|
|
8
|
+
* @evidence contracts/common.md#principled-implementation The media type is the text before the first `;`, trimmed and lower-cased as HTTP defines, and is matched against the binary families and two exact types, so a parameter or a case variant cannot change the decision.
|
|
9
|
+
* @evidence contracts/common.md#clear-and-simple-design One predicate with a type guard, so the caller can narrow the route's content type.
|
|
10
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The rule is a list of media-type families from the HTTP registry rather than a list of routes.
|
|
11
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the normalization and the binary types.
|
|
12
|
+
*/
|
|
1
13
|
export const is_binary_response_content_type = (
|
|
2
14
|
input: string | null | undefined,
|
|
3
15
|
): input is string => {
|