@kizunajs/fetch 2.0.0-beta.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/LICENSE.MD ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sondre Ørland
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @kizunajs/fetch
2
+
3
+ `@kizunajs/fetch` is the typed client for your routes, a wrapper around the native `fetch` API. It runs anywhere `fetch` does, React Native included.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pnpm add @kizunajs/fetch
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Add `fetchClient` to your config and run `kizuna generate`:
14
+
15
+ ```ts
16
+ import { fetchClient } from '@kizunajs/fetch/server';
17
+
18
+ export default defineConfig({
19
+ adapter: expressAdapter(),
20
+ routes: {
21
+ users,
22
+ },
23
+ clients: [
24
+ fetchClient({
25
+ output: './src/lib/api-client.generated.ts',
26
+ }),
27
+ ],
28
+ });
29
+ ```
30
+
31
+ The generated file exports `createClient`:
32
+
33
+ ```ts
34
+ import { createClient } from './api-client.generated';
35
+
36
+ const apiClient = createClient({
37
+ baseUrl: 'http://localhost:3000',
38
+ });
39
+
40
+ const result = await apiClient.users.getUser({
41
+ params: {
42
+ id: '1',
43
+ },
44
+ });
45
+ ```
46
+
47
+ ## Documentation
48
+
49
+ [Fetch client](https://kizunajs.com/docs/clients/fetch)
package/dist/index.cjs ADDED
@@ -0,0 +1,278 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let kizunajs = require("kizunajs");
3
+ //#region src/sse.ts
4
+ const parseData = (text) => {
5
+ try {
6
+ return JSON.parse(text);
7
+ } catch {
8
+ return text;
9
+ }
10
+ };
11
+ const findLineEnd = (buffer, final) => {
12
+ for (let index = 0; index < buffer.length; index += 1) {
13
+ const character = buffer[index];
14
+ if (character === "\n") return {
15
+ index,
16
+ length: 1
17
+ };
18
+ if (character === "\r") {
19
+ if (index + 1 < buffer.length) return {
20
+ index,
21
+ length: buffer[index + 1] === "\n" ? 2 : 1
22
+ };
23
+ if (final) return {
24
+ index,
25
+ length: 1
26
+ };
27
+ return;
28
+ }
29
+ }
30
+ };
31
+ /**
32
+ * Parses `text/event-stream` per the WHATWG HTML specification.
33
+ */
34
+ async function* parseServerSentEvents(body) {
35
+ const reader = body.getReader();
36
+ const decoder = new TextDecoder();
37
+ let buffer = "";
38
+ let first = true;
39
+ let dataLines = [];
40
+ let eventType = "";
41
+ let lastEventId = "";
42
+ let retry;
43
+ const dispatch = () => {
44
+ if (dataLines.length === 0) {
45
+ eventType = "";
46
+ return;
47
+ }
48
+ const message = { data: parseData(dataLines.join("\n")) };
49
+ if (eventType !== "") message.event = eventType;
50
+ if (lastEventId !== "") message.id = lastEventId;
51
+ if (retry !== void 0) message.retry = retry;
52
+ dataLines = [];
53
+ eventType = "";
54
+ retry = void 0;
55
+ return message;
56
+ };
57
+ const processLine = (line) => {
58
+ if (line === "") return dispatch();
59
+ if (line.startsWith(":")) return void 0;
60
+ const separator = line.indexOf(":");
61
+ const field = separator === -1 ? line : line.slice(0, separator);
62
+ let value = separator === -1 ? "" : line.slice(separator + 1);
63
+ if (value.startsWith(" ")) value = value.slice(1);
64
+ switch (field) {
65
+ case "event":
66
+ eventType = value;
67
+ break;
68
+ case "data":
69
+ dataLines.push(value);
70
+ break;
71
+ case "id":
72
+ if (!value.includes("\0")) lastEventId = value;
73
+ break;
74
+ case "retry":
75
+ if (/^\d+$/.test(value)) retry = Number(value);
76
+ break;
77
+ }
78
+ };
79
+ try {
80
+ for (;;) {
81
+ const { done, value } = await reader.read();
82
+ let chunk = done ? decoder.decode() : decoder.decode(value, { stream: true });
83
+ if (first && chunk.length > 0) {
84
+ if (chunk.charCodeAt(0) === 65279) chunk = chunk.slice(1);
85
+ first = false;
86
+ }
87
+ buffer += chunk;
88
+ for (;;) {
89
+ const lineEnd = findLineEnd(buffer, done);
90
+ if (lineEnd === void 0) break;
91
+ const line = buffer.slice(0, lineEnd.index);
92
+ buffer = buffer.slice(lineEnd.index + lineEnd.length);
93
+ const message = processLine(line);
94
+ if (message !== void 0) yield message;
95
+ }
96
+ if (done) return;
97
+ }
98
+ } finally {
99
+ reader.cancel().catch(() => void 0);
100
+ }
101
+ }
102
+ async function* readTextChunks(body) {
103
+ const reader = body.getReader();
104
+ const decoder = new TextDecoder();
105
+ try {
106
+ for (;;) {
107
+ const { done, value } = await reader.read();
108
+ if (done) {
109
+ const tail = decoder.decode();
110
+ if (tail.length > 0) yield tail;
111
+ return;
112
+ }
113
+ const text = decoder.decode(value, { stream: true });
114
+ if (text.length > 0) yield text;
115
+ }
116
+ } finally {
117
+ reader.cancel().catch(() => void 0);
118
+ }
119
+ }
120
+ async function* readByteChunks(body) {
121
+ const reader = body.getReader();
122
+ try {
123
+ for (;;) {
124
+ const { done, value } = await reader.read();
125
+ if (done) return;
126
+ yield value;
127
+ }
128
+ } finally {
129
+ reader.cancel().catch(() => void 0);
130
+ }
131
+ }
132
+ //#endregion
133
+ //#region src/client.ts
134
+ const buildFormData = (body) => {
135
+ const formData = new FormData();
136
+ for (const [key, entry] of Object.entries(body)) {
137
+ const values = Array.isArray(entry) ? entry : [entry];
138
+ for (const value of values) if (value instanceof File || value instanceof Blob) formData.append(key, value);
139
+ else if (value !== void 0 && value !== null) formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
140
+ }
141
+ return formData;
142
+ };
143
+ /**
144
+ * Serializes a query or path value for the URL. Dates use ISO 8601; everything
145
+ * else uses `String`.
146
+ */
147
+ const serializeValue = (value) => value instanceof Date ? value.toISOString() : String(value);
148
+ const buildQueryString = (query) => {
149
+ const params = new URLSearchParams();
150
+ for (const [key, value] of Object.entries(query)) {
151
+ if (value === void 0 || value === null) continue;
152
+ if (Array.isArray(value)) for (const item of value) params.append(key, serializeValue(item));
153
+ else params.append(key, serializeValue(value));
154
+ }
155
+ const result = params.toString();
156
+ return result.length > 0 ? `?${result}` : "";
157
+ };
158
+ const CLIENT_ROUTE = Symbol.for("kizuna.client-route");
159
+ /**
160
+ * The route a client method answers, or `undefined` for anything that is not
161
+ * one.
162
+ */
163
+ const routeOf = (value) => typeof value === "function" ? value[CLIENT_ROUTE] : void 0;
164
+ const buildRouteFn = (route, config) => {
165
+ const call = async (args = {}) => {
166
+ const url = config.baseUrl + (0, kizunajs.buildPath)(route.path, args.params) + (args.query ? buildQueryString(args.query) : "");
167
+ const headers = {
168
+ ...config.baseHeaders ?? {},
169
+ ...args.headers ?? {}
170
+ };
171
+ let body;
172
+ if (args.body !== void 0) {
173
+ const hasContentTypeHeader = "Content-Type" in headers || "content-type" in headers;
174
+ switch (route.contentType) {
175
+ case "multipart/form-data":
176
+ body = args.body instanceof FormData ? args.body : buildFormData(args.body);
177
+ break;
178
+ case "application/x-www-form-urlencoded":
179
+ if (!hasContentTypeHeader) headers["Content-Type"] = "application/x-www-form-urlencoded";
180
+ body = new URLSearchParams(args.body);
181
+ break;
182
+ default:
183
+ if (!hasContentTypeHeader) headers["Content-Type"] = "application/json";
184
+ body = JSON.stringify(args.body);
185
+ }
186
+ }
187
+ const requestHeaders = new Headers(headers);
188
+ if (config.onRequest) await config.onRequest({
189
+ url,
190
+ method: route.method,
191
+ headers: requestHeaders,
192
+ route
193
+ });
194
+ const res = await (config.fetch ?? fetch)(url, {
195
+ method: route.method,
196
+ headers: requestHeaders,
197
+ body,
198
+ credentials: config.credentials,
199
+ ...args.fetchOptions
200
+ });
201
+ const responseHeaders = {};
202
+ res.headers.forEach((value, key) => {
203
+ responseHeaders[key] = value;
204
+ });
205
+ const responseSpec = route.responses[res.status];
206
+ if ((0, kizunajs.isStreamResponse)(responseSpec) && res.body !== null) return {
207
+ status: res.status,
208
+ body: readStream(res.body, responseSpec),
209
+ headers: responseHeaders
210
+ };
211
+ const text = await res.text();
212
+ let parsed;
213
+ try {
214
+ parsed = text.length > 0 ? JSON.parse(text) : void 0;
215
+ } catch {
216
+ parsed = text;
217
+ }
218
+ return {
219
+ status: res.status,
220
+ body: parsed,
221
+ headers: responseHeaders
222
+ };
223
+ };
224
+ call[CLIENT_ROUTE] = route;
225
+ return call;
226
+ };
227
+ const readStream = (body, definition) => {
228
+ switch ((0, kizunajs.streamMode)(definition)) {
229
+ case "events": return parseServerSentEvents(body);
230
+ case "text": return readTextChunks(body);
231
+ case "binary": return readByteChunks(body);
232
+ }
233
+ };
234
+ const buildClientTree = (router, config) => {
235
+ const result = {};
236
+ for (const key of Object.keys(router)) {
237
+ const node = router[key];
238
+ if ((0, kizunajs.isRouteDefinition)(node)) result[key] = buildRouteFn(node, config);
239
+ else if (node && typeof node === "object") result[key] = buildClientTree(node, config);
240
+ }
241
+ return result;
242
+ };
243
+ /**
244
+ * Folds the request context headers into `baseHeaders`, so every request
245
+ * carries them. An explicit `baseHeaders` entry wins.
246
+ */
247
+ const withContextHeaders = (config) => {
248
+ const contextHeaders = config.requestContext;
249
+ if (!contextHeaders) return config;
250
+ return {
251
+ ...config,
252
+ baseHeaders: {
253
+ ...Object.fromEntries(Object.entries(contextHeaders).filter(([, value]) => value !== void 0)),
254
+ ...config.baseHeaders ?? {}
255
+ }
256
+ };
257
+ };
258
+ /**
259
+ * Builds the client tree a generated client exposes. The generated file owns the
260
+ * types; this owns the requests.
261
+ */
262
+ const createGeneratedClient = (routes, config) => buildClientTree(routes, withContextHeaders(config));
263
+ //#endregion
264
+ exports.CLIENT_ROUTE = CLIENT_ROUTE;
265
+ exports.createGeneratedClient = createGeneratedClient;
266
+ Object.defineProperty(exports, "isProblemDetails", {
267
+ enumerable: true,
268
+ get: function() {
269
+ return kizunajs.isProblemDetails;
270
+ }
271
+ });
272
+ Object.defineProperty(exports, "isValidationError", {
273
+ enumerable: true,
274
+ get: function() {
275
+ return kizunajs.isValidationError;
276
+ }
277
+ });
278
+ exports.routeOf = routeOf;
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ import { AUTO_GUARD_BRAND, AUTO_RESPONSES_BRAND, ExtractPathParams, HasPathParams, Method, ProblemDetails, RequestContextHeaderInputs, RouteDefinition, RoutePath, Routes, StreamMessageOf, StreamResponseDefinition, ValidationError, ValidationError as ValidationError$1, ValidationErrorFor, isProblemDetails, isValidationError } from "kizunajs";
3
+
4
+ //#region src/client.d.ts
5
+ type ResponseUnion<R extends RouteDefinition> = { [S in keyof R['responses']]: {
6
+ status: S extends number ? S : never;
7
+ body: R['responses'][S] extends z.ZodType ? z.infer<R['responses'][S]> : R['responses'][S] extends StreamResponseDefinition ? AsyncIterable<StreamMessageOf<R['responses'][S]>> : R['responses'][S] extends {
8
+ body: z.ZodType;
9
+ } ? z.infer<R['responses'][S]['body']> : never;
10
+ headers: R['responses'][S] extends {
11
+ headers: z.ZodType;
12
+ } ? z.infer<R['responses'][S]['headers']> : Record<string, string>;
13
+ } }[keyof R['responses']];
14
+ /**
15
+ * The type a caller passes for a body, query, or headers argument, the schema's
16
+ * input type.
17
+ */
18
+ type ClientPayload<T extends z.ZodType> = z.input<T>;
19
+ /**
20
+ * Path params are typed from the route's `pathParams` schema output when one is declared,
21
+ * mirroring the server-side `HandlerArgs`. This makes refinements like `.brand()` flow to
22
+ * the caller. Falls back to the path template (`:param` → `string`).
23
+ */
24
+ type ClientParams<R extends RouteDefinition> = R extends {
25
+ pathParams: z.ZodType;
26
+ } ? z.output<R['pathParams']> : ExtractPathParams<R['path']>;
27
+ /**
28
+ * True when the arg can be omitted: its input accepts `{}` or `undefined`.
29
+ */
30
+ type IsOmittable<Payload> = {} extends Payload ? true : [undefined] extends [Payload] ? true : false;
31
+ /**
32
+ * Everything a caller passes for one route: `params`, `body`, `query`, `headers`,
33
+ * and `fetchOptions`. Each key is present only when the route declares it, and
34
+ * optional when its input accepts `{}` or `undefined`.
35
+ */
36
+ type ClientArgs<R extends RouteDefinition> = (HasPathParams<R['path']> extends true ? {
37
+ params: ClientParams<R>;
38
+ } : {}) & (R extends {
39
+ body: z.ZodType;
40
+ } ? (ClientPayload<R['body']> extends void ? {} : {
41
+ body: ClientPayload<R['body']>;
42
+ }) : {}) & (R extends {
43
+ query: z.ZodType;
44
+ } ? IsOmittable<ClientPayload<R['query']>> extends true ? {
45
+ query?: ClientPayload<R['query']>;
46
+ } : {
47
+ query: ClientPayload<R['query']>;
48
+ } : {}) & (R extends {
49
+ headers: z.ZodType;
50
+ } ? IsOmittable<ClientPayload<R['headers']>> extends true ? {
51
+ headers?: ClientPayload<R['headers']>;
52
+ } : {
53
+ headers: ClientPayload<R['headers']>;
54
+ } : {
55
+ headers?: Record<string, string>;
56
+ }) & {
57
+ fetchOptions?: RequestInit;
58
+ };
59
+ type ValidationErrorResult<Codes extends string> = {
60
+ status: 400;
61
+ body: [Codes] extends [never] ? ValidationError$1 : ValidationErrorFor<Codes>;
62
+ headers: Record<string, string>;
63
+ };
64
+ type HasValidation<R extends RouteDefinition> = R extends {
65
+ body: z.ZodType;
66
+ } ? true : R extends {
67
+ query: z.ZodType;
68
+ } ? true : false;
69
+ /**
70
+ * The statuses the api's auth map put on a guarded route. A declared `403`
71
+ * sits alongside, as a declared `400` does beside the validation error.
72
+ */
73
+ type AutoStatuses<R extends RouteDefinition> = Extract<typeof AUTO_RESPONSES_BRAND extends keyof R ? NonNullable<R[typeof AUTO_RESPONSES_BRAND]> : never, number>;
74
+ /**
75
+ * The api's `guardSchema`, or plain Problem Details when it declares none.
76
+ */
77
+ type AutoBody<R extends RouteDefinition> = typeof AUTO_GUARD_BRAND extends keyof R ? NonNullable<R[typeof AUTO_GUARD_BRAND]> : ProblemDetails;
78
+ type AutoErrorResult<R extends RouteDefinition> = { [Status in AutoStatuses<R>]: {
79
+ status: Status;
80
+ body: AutoBody<R>;
81
+ headers: Record<string, string>;
82
+ } }[AutoStatuses<R>];
83
+ /**
84
+ * Every response one route can produce, as a union discriminated on `status`.
85
+ * Routes with a `body` or `query` schema also carry the automatic `400`, and
86
+ * guarded routes the `401` and `403`.
87
+ */
88
+ type ClientResponse<R extends RouteDefinition, Codes extends string = never> = (HasValidation<R> extends true ? ResponseUnion<R> | ValidationErrorResult<Codes> : ResponseUnion<R>) | AutoErrorResult<R>;
89
+ type ClientFn<R extends RouteDefinition, Codes extends string> = {} extends ClientArgs<R> ? (args?: ClientArgs<R>) => Promise<ClientResponse<R, Codes>> : (args: ClientArgs<R>) => Promise<ClientResponse<R, Codes>>;
90
+ type Client<T extends Routes, Codes extends string = never> = { [K in keyof T]: T[K] extends RouteDefinition ? ClientFn<T[K], Codes> : T[K] extends Routes ? Client<T[K], Codes> : never };
91
+ type UnionToIntersection<Union> = (Union extends unknown ? (distributed: Union) => void : never) extends ((intersected: infer Intersection) => void) ? Intersection : never;
92
+ /**
93
+ * Every header input the api's request context declares, flattened. Set
94
+ * once on the client under `requestContext` and sent with every request.
95
+ */
96
+ type ContextHeaderInputs<Declarations> = string extends keyof Declarations ? {} : [keyof Declarations] extends [never] ? {} : UnionToIntersection<{ [Name in keyof Declarations]: RequestContextHeaderInputs<Declarations[Name]> }[keyof Declarations]> extends infer Merged ? { [Key in keyof Merged]: Merged[Key] } : never;
97
+ /**
98
+ * The request `onRequest` is handed, on its way out. Setting a header on
99
+ * `headers` sets it on the request that is sent.
100
+ */
101
+ interface OutgoingRequest {
102
+ url: string;
103
+ method: string;
104
+ headers: Headers;
105
+ route: RouteDefinition;
106
+ }
107
+ interface ClientConfig {
108
+ baseUrl: string;
109
+ baseHeaders?: Record<string, string>;
110
+ credentials?: RequestCredentials;
111
+ fetch?: typeof fetch;
112
+ onRequest?: (request: OutgoingRequest) => void | Promise<void>;
113
+ }
114
+ /**
115
+ * Registry-global: what a client method was built from. Anything wrapping a
116
+ * client reads the route here rather than being handed the api a second time.
117
+ */
118
+ /**
119
+ * One method on a generated client: the call itself, carrying the method it
120
+ * sends and whether that response streams. A wrapper reads those off the client
121
+ * rather than being handed the routes a second time.
122
+ */
123
+ type ClientMethod<Method extends string, Streams extends boolean, Args, Result> = ({} extends Args ? (args?: Args) => Promise<Result> : (args: Args) => Promise<Result>) & {
124
+ readonly [CLIENT_ROUTE]?: {
125
+ readonly method: Method;
126
+ readonly streams: Streams;
127
+ };
128
+ };
129
+ declare const CLIENT_ROUTE: unique symbol;
130
+ /**
131
+ * The route a client method answers, or `undefined` for anything that is not
132
+ * one.
133
+ */
134
+ declare const routeOf: (value: unknown) => RouteDefinition | undefined;
135
+ /**
136
+ * What a generated client knows about one response: nothing for a body it
137
+ * parses as JSON, the media type for one it streams.
138
+ */
139
+ type GeneratedResponse = Record<string, never> | {
140
+ stream: {
141
+ contentType?: string;
142
+ };
143
+ };
144
+ /**
145
+ * One route in a generated client's table: how to reach it, and enough about
146
+ * each response to read the body.
147
+ */
148
+ interface GeneratedRoute {
149
+ method: Method;
150
+ path: RoutePath;
151
+ contentType?: string;
152
+ responses: Record<number, GeneratedResponse>;
153
+ }
154
+ /**
155
+ * The route table a generated client carries, nested the way the client is.
156
+ */
157
+ interface GeneratedRoutes {
158
+ [key: string]: GeneratedRoutes | GeneratedRoute;
159
+ }
160
+ /**
161
+ * Builds the client tree a generated client exposes. The generated file owns the
162
+ * types; this owns the requests.
163
+ */
164
+ declare const createGeneratedClient: (routes: GeneratedRoutes, config: ClientConfig) => Record<string, unknown>;
165
+ //#endregion
166
+ export { CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };
@@ -0,0 +1,166 @@
1
+ import { AUTO_GUARD_BRAND, AUTO_RESPONSES_BRAND, ExtractPathParams, HasPathParams, Method, ProblemDetails, RequestContextHeaderInputs, RouteDefinition, RoutePath, Routes, StreamMessageOf, StreamResponseDefinition, ValidationError, ValidationError as ValidationError$1, ValidationErrorFor, isProblemDetails, isValidationError } from "kizunajs";
2
+ import { z } from "zod";
3
+
4
+ //#region src/client.d.ts
5
+ type ResponseUnion<R extends RouteDefinition> = { [S in keyof R['responses']]: {
6
+ status: S extends number ? S : never;
7
+ body: R['responses'][S] extends z.ZodType ? z.infer<R['responses'][S]> : R['responses'][S] extends StreamResponseDefinition ? AsyncIterable<StreamMessageOf<R['responses'][S]>> : R['responses'][S] extends {
8
+ body: z.ZodType;
9
+ } ? z.infer<R['responses'][S]['body']> : never;
10
+ headers: R['responses'][S] extends {
11
+ headers: z.ZodType;
12
+ } ? z.infer<R['responses'][S]['headers']> : Record<string, string>;
13
+ } }[keyof R['responses']];
14
+ /**
15
+ * The type a caller passes for a body, query, or headers argument, the schema's
16
+ * input type.
17
+ */
18
+ type ClientPayload<T extends z.ZodType> = z.input<T>;
19
+ /**
20
+ * Path params are typed from the route's `pathParams` schema output when one is declared,
21
+ * mirroring the server-side `HandlerArgs`. This makes refinements like `.brand()` flow to
22
+ * the caller. Falls back to the path template (`:param` → `string`).
23
+ */
24
+ type ClientParams<R extends RouteDefinition> = R extends {
25
+ pathParams: z.ZodType;
26
+ } ? z.output<R['pathParams']> : ExtractPathParams<R['path']>;
27
+ /**
28
+ * True when the arg can be omitted: its input accepts `{}` or `undefined`.
29
+ */
30
+ type IsOmittable<Payload> = {} extends Payload ? true : [undefined] extends [Payload] ? true : false;
31
+ /**
32
+ * Everything a caller passes for one route: `params`, `body`, `query`, `headers`,
33
+ * and `fetchOptions`. Each key is present only when the route declares it, and
34
+ * optional when its input accepts `{}` or `undefined`.
35
+ */
36
+ type ClientArgs<R extends RouteDefinition> = (HasPathParams<R['path']> extends true ? {
37
+ params: ClientParams<R>;
38
+ } : {}) & (R extends {
39
+ body: z.ZodType;
40
+ } ? (ClientPayload<R['body']> extends void ? {} : {
41
+ body: ClientPayload<R['body']>;
42
+ }) : {}) & (R extends {
43
+ query: z.ZodType;
44
+ } ? IsOmittable<ClientPayload<R['query']>> extends true ? {
45
+ query?: ClientPayload<R['query']>;
46
+ } : {
47
+ query: ClientPayload<R['query']>;
48
+ } : {}) & (R extends {
49
+ headers: z.ZodType;
50
+ } ? IsOmittable<ClientPayload<R['headers']>> extends true ? {
51
+ headers?: ClientPayload<R['headers']>;
52
+ } : {
53
+ headers: ClientPayload<R['headers']>;
54
+ } : {
55
+ headers?: Record<string, string>;
56
+ }) & {
57
+ fetchOptions?: RequestInit;
58
+ };
59
+ type ValidationErrorResult<Codes extends string> = {
60
+ status: 400;
61
+ body: [Codes] extends [never] ? ValidationError$1 : ValidationErrorFor<Codes>;
62
+ headers: Record<string, string>;
63
+ };
64
+ type HasValidation<R extends RouteDefinition> = R extends {
65
+ body: z.ZodType;
66
+ } ? true : R extends {
67
+ query: z.ZodType;
68
+ } ? true : false;
69
+ /**
70
+ * The statuses the api's auth map put on a guarded route. A declared `403`
71
+ * sits alongside, as a declared `400` does beside the validation error.
72
+ */
73
+ type AutoStatuses<R extends RouteDefinition> = Extract<typeof AUTO_RESPONSES_BRAND extends keyof R ? NonNullable<R[typeof AUTO_RESPONSES_BRAND]> : never, number>;
74
+ /**
75
+ * The api's `guardSchema`, or plain Problem Details when it declares none.
76
+ */
77
+ type AutoBody<R extends RouteDefinition> = typeof AUTO_GUARD_BRAND extends keyof R ? NonNullable<R[typeof AUTO_GUARD_BRAND]> : ProblemDetails;
78
+ type AutoErrorResult<R extends RouteDefinition> = { [Status in AutoStatuses<R>]: {
79
+ status: Status;
80
+ body: AutoBody<R>;
81
+ headers: Record<string, string>;
82
+ } }[AutoStatuses<R>];
83
+ /**
84
+ * Every response one route can produce, as a union discriminated on `status`.
85
+ * Routes with a `body` or `query` schema also carry the automatic `400`, and
86
+ * guarded routes the `401` and `403`.
87
+ */
88
+ type ClientResponse<R extends RouteDefinition, Codes extends string = never> = (HasValidation<R> extends true ? ResponseUnion<R> | ValidationErrorResult<Codes> : ResponseUnion<R>) | AutoErrorResult<R>;
89
+ type ClientFn<R extends RouteDefinition, Codes extends string> = {} extends ClientArgs<R> ? (args?: ClientArgs<R>) => Promise<ClientResponse<R, Codes>> : (args: ClientArgs<R>) => Promise<ClientResponse<R, Codes>>;
90
+ type Client<T extends Routes, Codes extends string = never> = { [K in keyof T]: T[K] extends RouteDefinition ? ClientFn<T[K], Codes> : T[K] extends Routes ? Client<T[K], Codes> : never };
91
+ type UnionToIntersection<Union> = (Union extends unknown ? (distributed: Union) => void : never) extends ((intersected: infer Intersection) => void) ? Intersection : never;
92
+ /**
93
+ * Every header input the api's request context declares, flattened. Set
94
+ * once on the client under `requestContext` and sent with every request.
95
+ */
96
+ type ContextHeaderInputs<Declarations> = string extends keyof Declarations ? {} : [keyof Declarations] extends [never] ? {} : UnionToIntersection<{ [Name in keyof Declarations]: RequestContextHeaderInputs<Declarations[Name]> }[keyof Declarations]> extends infer Merged ? { [Key in keyof Merged]: Merged[Key] } : never;
97
+ /**
98
+ * The request `onRequest` is handed, on its way out. Setting a header on
99
+ * `headers` sets it on the request that is sent.
100
+ */
101
+ interface OutgoingRequest {
102
+ url: string;
103
+ method: string;
104
+ headers: Headers;
105
+ route: RouteDefinition;
106
+ }
107
+ interface ClientConfig {
108
+ baseUrl: string;
109
+ baseHeaders?: Record<string, string>;
110
+ credentials?: RequestCredentials;
111
+ fetch?: typeof fetch;
112
+ onRequest?: (request: OutgoingRequest) => void | Promise<void>;
113
+ }
114
+ /**
115
+ * Registry-global: what a client method was built from. Anything wrapping a
116
+ * client reads the route here rather than being handed the api a second time.
117
+ */
118
+ /**
119
+ * One method on a generated client: the call itself, carrying the method it
120
+ * sends and whether that response streams. A wrapper reads those off the client
121
+ * rather than being handed the routes a second time.
122
+ */
123
+ type ClientMethod<Method extends string, Streams extends boolean, Args, Result> = ({} extends Args ? (args?: Args) => Promise<Result> : (args: Args) => Promise<Result>) & {
124
+ readonly [CLIENT_ROUTE]?: {
125
+ readonly method: Method;
126
+ readonly streams: Streams;
127
+ };
128
+ };
129
+ declare const CLIENT_ROUTE: unique symbol;
130
+ /**
131
+ * The route a client method answers, or `undefined` for anything that is not
132
+ * one.
133
+ */
134
+ declare const routeOf: (value: unknown) => RouteDefinition | undefined;
135
+ /**
136
+ * What a generated client knows about one response: nothing for a body it
137
+ * parses as JSON, the media type for one it streams.
138
+ */
139
+ type GeneratedResponse = Record<string, never> | {
140
+ stream: {
141
+ contentType?: string;
142
+ };
143
+ };
144
+ /**
145
+ * One route in a generated client's table: how to reach it, and enough about
146
+ * each response to read the body.
147
+ */
148
+ interface GeneratedRoute {
149
+ method: Method;
150
+ path: RoutePath;
151
+ contentType?: string;
152
+ responses: Record<number, GeneratedResponse>;
153
+ }
154
+ /**
155
+ * The route table a generated client carries, nested the way the client is.
156
+ */
157
+ interface GeneratedRoutes {
158
+ [key: string]: GeneratedRoutes | GeneratedRoute;
159
+ }
160
+ /**
161
+ * Builds the client tree a generated client exposes. The generated file owns the
162
+ * types; this owns the requests.
163
+ */
164
+ declare const createGeneratedClient: (routes: GeneratedRoutes, config: ClientConfig) => Record<string, unknown>;
165
+ //#endregion
166
+ export { CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };