tanstack-fetch 1.0.0 → 1.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/README.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # tanstack-fetch
2
2
 
3
- Typed `fetch` client shaped for **TanStack Query**.
3
+ Typed `fetch` client shaped for **TanStack Query** — tiny HTTP core, optional SSE / React.
4
4
 
5
- `queryFn` / `mutationFn` ready by default: returns **data**, throws **`FetchError`**, passes **`signal`**. Works in browser, SSR, and Edge — with SSE, named interceptors, and OpenAPI codegen.
5
+ | Import | What you get | Typical gzip |
6
+ | --- | --- | --- |
7
+ | `tanstack-fetch` | HTTP only (`get/post/…`) | **~3.5KB** |
8
+ | `tanstack-fetch/sse` | + `api.sse()` | **~4.7KB** |
9
+ | `tanstack-fetch/plugins` | plugin factories | **~0.9KB** |
10
+ | `tanstack-fetch/react` | `FetchProvider` / hooks (peer: core) | **~1KB** |
6
11
 
7
12
  ```bash
8
13
  npm install tanstack-fetch
@@ -14,6 +19,24 @@ Node 18+ (native `fetch`).
14
19
 
15
20
  ---
16
21
 
22
+ ## Bundle size
23
+
24
+ Tree-shake by importing only what you need:
25
+
26
+ ```ts
27
+ // smallest — HTTP for TanStack Query
28
+ import { createFetch } from 'tanstack-fetch'
29
+
30
+ // only when you need streams
31
+ import { createFetch } from 'tanstack-fetch/sse'
32
+ ```
33
+
34
+ `yaml` is an **optional** peer (CLI YAML specs only). React is optional too.
35
+
36
+ Run `npm run size` after build to print local gzip numbers.
37
+
38
+ ---
39
+
17
40
  ## Two ways to configure
18
41
 
19
42
  ### 1) Simple path — `baseUrl`, token, status handlers
@@ -451,6 +474,87 @@ await api.request<User>('GET', '/users/:id', { params: { id: '1' } })
451
474
 
452
475
  ---
453
476
 
477
+ ## Upload
478
+
479
+ `FormData` / `Blob` / `File` are sent as-is (no JSON, no forced `Content-Type` — the boundary stays correct).
480
+
481
+ The call still returns the **typed response body** from the server (same as `post`), not a special upload envelope.
482
+
483
+ ### `api.upload()` — file + fields + progress
484
+
485
+ ```ts
486
+ type UploadResponse = { id: string; url: string }
487
+
488
+ const file = input.files[0]
489
+
490
+ const uploaded = await api.upload<UploadResponse>('/files', {
491
+ file,
492
+ fieldName: 'avatar', // default: 'file'
493
+ fields: { folder: 'avatars', public: true },
494
+ onUploadProgress: ({ loaded, total, progress }) => {
495
+ // progress is 0–1 when total is known (browser / XHR)
496
+ console.log(loaded, total, progress)
497
+ },
498
+ })
499
+
500
+ uploaded.url
501
+ ```
502
+
503
+ Multiple files or raw `FormData`:
504
+
505
+ ```ts
506
+ await api.upload('/docs', {
507
+ method: 'PUT',
508
+ files: [fileA, fileB],
509
+ fieldName: 'docs',
510
+ })
511
+
512
+ await api.upload<UploadResponse>('/files', {
513
+ body: createFormData({ file, note: 'cv' }),
514
+ })
515
+ ```
516
+
517
+ ### `createFormData` helper
518
+
519
+ ```ts
520
+ import { createFetch, createFormData } from 'tanstack-fetch'
521
+
522
+ const api = createFetch({ baseUrl: 'https://api.example.com' })
523
+
524
+ const body = createFormData({
525
+ title: 'Report',
526
+ tags: ['a', 'b'], // repeated field
527
+ file,
528
+ })
529
+
530
+ await api.post<UploadResponse>('/files', {
531
+ body,
532
+ onUploadProgress: ({ progress }) => console.log(progress),
533
+ })
534
+ ```
535
+
536
+ | Option | Notes |
537
+ | --- | --- |
538
+ | `file` / `files` | Appended under `fieldName` (default `"file"`) |
539
+ | `fields` | Extra multipart values (string / number / boolean / `Blob` / arrays) |
540
+ | `body` | Pre-built `FormData` / `Blob` / … |
541
+ | `method` | `POST` (default), `PUT`, or `PATCH` |
542
+ | `onUploadProgress` | Browser-only — uses XHR under the hood (`fetch` has no upload progress). No-ops on runtimes without `XMLHttpRequest` (falls back to `fetch`) |
543
+
544
+ Works with TanStack Query mutations the same way as `post`:
545
+
546
+ ```ts
547
+ useMutation({
548
+ mutationFn: (file: File) =>
549
+ api.upload<UploadResponse>('/files', {
550
+ file,
551
+ onUploadProgress: ({ progress }) => setProgress(progress ?? 0),
552
+ }),
553
+ })
554
+ ```
555
+
556
+ ---
557
+
454
558
  ## `FetchError`
455
559
 
456
560
  ```ts
@@ -555,6 +659,8 @@ await api.post('/orders', { body: { sku: 'A' } }) // not retried
555
659
  ### `sse-resume`
556
660
 
557
661
  ```ts
662
+ import { createFetch } from 'tanstack-fetch/sse'
663
+
558
664
  const api = createFetch({
559
665
  baseUrl: 'https://api.example.com',
560
666
  plugins: ['sse-resume'],
@@ -703,6 +809,13 @@ Uses `fetch` streams (not `EventSource`) — Authorization, cookies, and SSR wor
703
809
  ### Simple — `onMessage`
704
810
 
705
811
  ```ts
812
+ import { createFetch } from 'tanstack-fetch/sse'
813
+
814
+ const api = createFetch({
815
+ baseUrl: 'https://api.example.com',
816
+ plugins: ['sse-resume'],
817
+ })
818
+
706
819
  const stream = api.sse<OrderEvent>('/orders/stream', {
707
820
  onMessage: (data) => {
708
821
  console.log(data) // just the payload
@@ -716,8 +829,22 @@ stream.close()
716
829
 
717
830
  ### React — `useSse`
718
831
 
832
+ Pass a client created from `tanstack-fetch/sse`:
833
+
719
834
  ```tsx
720
- import { useSse } from 'tanstack-fetch/react'
835
+ import { createFetch } from 'tanstack-fetch/sse'
836
+ import { FetchProvider, useSse } from 'tanstack-fetch/react'
837
+
838
+ const api = createFetch({
839
+ baseUrl: import.meta.env.VITE_API_URL,
840
+ plugins: ['sse-resume'],
841
+ })
842
+
843
+ const App = () => (
844
+ <FetchProvider client={api}>
845
+ <OrdersLive />
846
+ </FetchProvider>
847
+ )
721
848
 
722
849
  const OrdersLive = () => {
723
850
  const { data, isConnected, error } = useSse<OrderEvent>('/orders/stream')
@@ -734,6 +861,10 @@ const OrdersLive = () => {
734
861
  ### Advanced — `for await`
735
862
 
736
863
  ```ts
864
+ import { createFetch } from 'tanstack-fetch/sse'
865
+
866
+ const api = createFetch({ baseUrl: 'https://api.example.com' })
867
+
737
868
  for await (const event of api.sse<OrderEvent>('/orders/stream', { signal })) {
738
869
  event.event
739
870
  event.data
package/dist/cli.js CHANGED
@@ -1,234 +1,35 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli/generate.ts
4
- import { mkdir, writeFile } from "fs/promises";
5
- import { join } from "path";
6
-
7
- // src/cli/schema-to-ts.ts
8
- var toPascal = (value) => value.replace(/[^A-Za-z0-9]+/g, " ").trim().split(" ").filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("") || "Schema";
9
- var toCamel = (value) => {
10
- const pascal = toPascal(value);
11
- return pascal[0].toLowerCase() + pascal.slice(1);
12
- };
13
- var refName = (ref) => {
14
- const parts = ref.split("/");
15
- return toPascal(parts[parts.length - 1] ?? "Schema");
16
- };
17
- var schemaToTs = (schema) => {
18
- if (!schema) {
19
- return "unknown";
20
- }
21
- if (schema.$ref) {
22
- return refName(schema.$ref);
23
- }
24
- if (schema.enum && schema.enum.length > 0) {
25
- return schema.enum.map((value) => JSON.stringify(value)).join(" | ");
26
- }
27
- const typeValue = Array.isArray(schema.type) ? schema.type[0] : schema.type;
28
- if (typeValue === "string") {
29
- return "string";
30
- }
31
- if (typeValue === "integer" || typeValue === "number") {
32
- return "number";
33
- }
34
- if (typeValue === "boolean") {
35
- return "boolean";
36
- }
37
- if (typeValue === "array") {
38
- return `Array<${schemaToTs(schema.items)}>`;
39
- }
40
- if (typeValue === "object" || schema.properties) {
41
- return objectToTs(schema);
42
- }
43
- return "unknown";
44
- };
45
- var objectToTs = (schema) => {
46
- const required = new Set(schema.required ?? []);
47
- const fields = Object.entries(schema.properties ?? {}).map(([key, value]) => {
48
- const optional = required.has(key) ? "" : "?";
49
- return ` ${key}${optional}: ${schemaToTs(value)}`;
50
- });
51
- if (fields.length === 0) {
52
- return "Record<string, unknown>";
53
- }
54
- return `{
55
- ${fields.join("\n")}
56
- }`;
57
- };
58
-
59
- // src/cli/collect-operations.ts
60
- var HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
61
- var isSseOperation = (operation) => {
62
- const contents = Object.values(operation.responses ?? {}).flatMap(
63
- (response) => Object.keys(response.content ?? {})
64
- );
65
- return contents.some((type) => type.includes("event-stream"));
66
- };
67
- var collectOperations = (spec) => {
68
- const operations = [];
69
- Object.entries(spec.paths ?? {}).forEach(([path, methods]) => {
70
- HTTP_METHODS.forEach((method) => {
71
- const operation = methods?.[method];
72
- if (!operation) {
73
- return;
74
- }
75
- const tag = operation.tags?.[0] ?? "api";
76
- const fallbackId = `${method}_${path}`;
77
- operations.push({
78
- method: method.toUpperCase(),
79
- path,
80
- operationId: toCamel(operation.operationId ?? fallbackId),
81
- tag: toCamel(tag),
82
- isSse: isSseOperation(operation),
83
- parameters: operation.parameters ?? [],
84
- bodySchema: operation.requestBody?.content?.["application/json"]?.schema,
85
- successSchema: pickSuccessSchema(operation)
86
- });
87
- });
88
- });
89
- return operations;
90
- };
91
- var pickSuccessSchema = (operation) => {
92
- const success = operation.responses?.["200"] ?? operation.responses?.["201"];
93
- const content = success?.content ?? {};
94
- return content["application/json"]?.schema ?? content["text/event-stream"]?.schema;
95
- };
96
-
97
- // src/cli/generate.ts
98
- var generateTypesFile = (spec) => {
99
- const schemas = Object.entries(spec.components?.schemas ?? {});
100
- const types = schemas.map(([name, schema]) => `type ${toPascal(name)} = ${schemaToTs(schema)}`);
101
- const exports = schemas.map(([name]) => toPascal(name));
102
- if (exports.length === 0) {
103
- return "export {}\n";
104
- }
105
- const body = `${types.join("\n\n")}
106
-
107
- export type { ${exports.join(", ")} }
108
- `;
109
- return body.trimStart();
110
- };
111
- var paramsType = (operation, kind) => {
112
- const params = operation.parameters.filter((item) => item.in === kind);
113
- if (params.length === 0) {
114
- return void 0;
115
- }
116
- const fields = params.map((item) => {
117
- const optional = item.required ? "" : "?";
118
- return `${item.name}${optional}: ${schemaToTs(item.schema)}`;
119
- });
120
- return `{ ${fields.join("; ")} }`;
121
- };
122
- var optionsType = (operation) => {
123
- const fields = [];
124
- const pathType = paramsType(operation, "path");
125
- const queryType = paramsType(operation, "query");
126
- if (pathType) {
127
- fields.push(`params: ${pathType}`);
128
- }
129
- if (queryType) {
130
- fields.push(`query?: ${queryType}`);
131
- }
132
- if (operation.bodySchema) {
133
- fields.push(`body: ${schemaToTs(operation.bodySchema)}`);
134
- }
135
- if (fields.length === 0) {
136
- return "RequestOptions | undefined";
137
- }
138
- return `Omit<RequestOptions, 'params' | 'query' | 'body'> & { ${fields.join("; ")} }`;
139
- };
140
- var clientPath = (path) => path.replace(/\{([A-Za-z0-9_]+)\}/g, ":$1");
141
- var methodLine = (operation) => {
142
- const response = schemaToTs(operation.successSchema);
143
- const options = optionsType(operation);
144
- const optional = options.endsWith("| undefined") ? "?" : "";
145
- if (operation.isSse) {
146
- return ` ${operation.operationId}: (options${optional}: ${options}) => api.sse<${response}>('${clientPath(operation.path)}', options),`;
147
- }
148
- const method = operation.method.toLowerCase();
149
- const call = method === "delete" ? "delete" : method;
150
- return ` ${operation.operationId}: (options${optional}: ${options}) => api.${call}<${response}>('${clientPath(operation.path)}', options),`;
151
- };
152
- var generateClientFile = (spec, operations) => {
153
- const tags = [...new Set(operations.map((item) => item.tag))];
154
- const groups = tags.map((tag) => {
155
- const lines = operations.filter((item) => item.tag === tag).map(methodLine);
156
- return ` ${tag}: {
157
- ${lines.join("\n")}
158
- },`;
159
- });
160
- const schemaNames = Object.keys(spec.components?.schemas ?? {}).map((name) => toPascal(name));
161
- const typesImport = schemaNames.length > 0 ? `import type { ${schemaNames.join(", ")} } from './types'
162
- ` : "";
163
- return `import { createFetch } from 'tanstack-fetch'
164
- import type { CreateFetchOptions, RequestOptions } from 'tanstack-fetch'
165
- ${typesImport}
2
+ import{mkdir as T,writeFile as f}from"fs/promises";import{join as h}from"path";var i=e=>e.replace(/[^A-Za-z0-9]+/g," ").trim().split(" ").filter(Boolean).map(t=>t[0].toUpperCase()+t.slice(1)).join("")||"Schema",u=e=>{let t=i(e);return t[0].toLowerCase()+t.slice(1)},k=e=>{let t=e.split("/");return i(t[t.length-1]??"Schema")},p=e=>{if(!e)return"unknown";if(e.$ref)return k(e.$ref);if(e.enum&&e.enum.length>0)return e.enum.map(n=>JSON.stringify(n)).join(" | ");let t=Array.isArray(e.type)?e.type[0]:e.type;return t==="string"?"string":t==="integer"||t==="number"?"number":t==="boolean"?"boolean":t==="array"?`Array<${p(e.items)}>`:t==="object"||e.properties?C(e):"unknown"},C=e=>{let t=new Set(e.required??[]),n=Object.entries(e.properties??{}).map(([r,s])=>{let o=t.has(r)?"":"?";return` ${r}${o}: ${p(s)}`});return n.length===0?"Record<string, unknown>":`{
3
+ ${n.join(`
4
+ `)}
5
+ }`};var j=["get","post","put","patch","delete"],b=e=>Object.values(e.responses??{}).flatMap(n=>Object.keys(n.content??{})).some(n=>n.includes("event-stream")),d=e=>{let t=[];return Object.entries(e.paths??{}).forEach(([n,r])=>{j.forEach(s=>{let o=r?.[s];if(!o)return;let a=o.tags?.[0]??"api",m=`${s}_${n}`;t.push({method:s.toUpperCase(),path:n,operationId:u(o.operationId??m),tag:u(a),isSse:b(o),parameters:o.parameters??[],bodySchema:o.requestBody?.content?.["application/json"]?.schema,successSchema:x(o)})})}),t},x=e=>{let n=(e.responses?.["200"]??e.responses?.["201"])?.content??{};return n["application/json"]?.schema??n["text/event-stream"]?.schema};var q=e=>{let t=Object.entries(e.components?.schemas??{}),n=t.map(([o,a])=>`type ${i(o)} = ${p(a)}`),r=t.map(([o])=>i(o));return r.length===0?`export {}
6
+ `:`${n.join(`
7
+
8
+ `)}
9
+
10
+ export type { ${r.join(", ")} }
11
+ `.trimStart()},g=(e,t)=>{let n=e.parameters.filter(s=>s.in===t);return n.length===0?void 0:`{ ${n.map(s=>{let o=s.required?"":"?";return`${s.name}${o}: ${p(s.schema)}`}).join("; ")} }`},F=e=>{let t=[],n=g(e,"path"),r=g(e,"query");return n&&t.push(`params: ${n}`),r&&t.push(`query?: ${r}`),e.bodySchema&&t.push(`body: ${p(e.bodySchema)}`),t.length===0?"RequestOptions | undefined":`Omit<RequestOptions, 'params' | 'query' | 'body'> & { ${t.join("; ")} }`},y=e=>e.replace(/\{([A-Za-z0-9_]+)\}/g,":$1"),I=e=>{let t=p(e.successSchema),n=F(e),r=n.endsWith("| undefined")?"?":"";if(e.isSse)return` ${e.operationId}: (options${r}: ${n}) => api.sse<${t}>('${y(e.path)}', options),`;let s=e.method.toLowerCase(),o=s==="delete"?"delete":s;return` ${e.operationId}: (options${r}: ${n}) => api.${o}<${t}>('${y(e.path)}', options),`},E=(e,t)=>{let r=[...new Set(t.map(c=>c.tag))].map(c=>{let l=t.filter(A=>A.tag===c).map(I);return` ${c}: {
12
+ ${l.join(`
13
+ `)}
14
+ },`}),s=Object.keys(e.components?.schemas??{}).map(c=>i(c)),o=s.length>0?`import type { ${s.join(", ")} } from './types'
15
+ `:"";return`${t.some(c=>c.isSse)?`import { createFetch } from 'tanstack-fetch/sse'
16
+ import type { CreateFetchOptions, RequestOptions } from 'tanstack-fetch'`:`import { createFetch } from 'tanstack-fetch'
17
+ import type { CreateFetchOptions, RequestOptions } from 'tanstack-fetch'`}
18
+ ${o}
166
19
  const createApi = (options: CreateFetchOptions = {}) => {
167
20
  const api = createFetch(options)
168
21
  return {
169
- ${groups.join("\n")}
22
+ ${r.join(`
23
+ `)}
170
24
  }
171
25
  }
172
26
 
173
27
  export { createApi }
174
- `;
175
- };
176
- var generateIndexFile = () => `import { createApi } from './client'
28
+ `},J=()=>`import { createApi } from './client'
177
29
 
178
30
  export { createApi }
179
31
  export type * from './types'
180
- `;
181
- var generateClient = async (spec, outDir) => {
182
- const operations = collectOperations(spec);
183
- await mkdir(outDir, { recursive: true });
184
- await writeFile(join(outDir, "types.ts"), generateTypesFile(spec), "utf8");
185
- await writeFile(join(outDir, "client.ts"), generateClientFile(spec, operations), "utf8");
186
- await writeFile(join(outDir, "index.ts"), generateIndexFile(), "utf8");
187
- };
188
-
189
- // src/cli/load-spec.ts
190
- import { readFile } from "fs/promises";
191
- import { parse as parseYaml } from "yaml";
192
- var loadSpec = async (specPath) => {
193
- const raw = await readFile(specPath, "utf8");
194
- const parsed = specPath.endsWith(".yaml") || specPath.endsWith(".yml") ? parseYaml(raw) : JSON.parse(raw);
195
- if (!parsed || typeof parsed !== "object") {
196
- throw new Error("tanstack-fetch: OpenAPI spec must be an object");
197
- }
198
- return parsed;
199
- };
200
-
201
- // src/cli/parse-args.ts
202
- var parseArgs = (argv) => {
203
- const [command, ...rest] = argv;
204
- if (!command || command === "--help" || command === "help" || command === "-h") {
205
- return { command: "help" };
206
- }
207
- if (command !== "generate") {
208
- throw new Error(`tanstack-fetch: unknown command "${command}"`);
209
- }
210
- const flags = /* @__PURE__ */ new Map();
211
- for (let index = 0; index < rest.length; index += 1) {
212
- const token = rest[index];
213
- if (!token.startsWith("--")) {
214
- continue;
215
- }
216
- const key = token.slice(2);
217
- const value = rest[index + 1];
218
- if (!value || value.startsWith("--")) {
219
- throw new Error(`tanstack-fetch: missing value for --${key}`);
220
- }
221
- flags.set(key, value);
222
- index += 1;
223
- }
224
- const spec = flags.get("spec");
225
- const out = flags.get("out");
226
- if (!spec || !out) {
227
- throw new Error("tanstack-fetch: generate requires --spec and --out");
228
- }
229
- return { command: "generate", spec, out };
230
- };
231
- var helpText = `tanstack-fetch
32
+ `,O=async(e,t)=>{let n=d(e);await T(t,{recursive:!0}),await f(h(t,"types.ts"),q(e),"utf8"),await f(h(t,"client.ts"),E(e,n),"utf8"),await f(h(t,"index.ts"),J(),"utf8")};import{readFile as v}from"fs/promises";var L=async e=>{try{return(await import("yaml")).parse(e)}catch{throw new Error('tanstack-fetch: install optional peer "yaml" to load YAML specs (npm i yaml)')}},$=async e=>{let t=await v(e,"utf8"),n=e.endsWith(".yaml")||e.endsWith(".yml")?await L(t):JSON.parse(t);if(!n||typeof n!="object")throw new Error("tanstack-fetch: OpenAPI spec must be an object");return n};var S=e=>{let[t,...n]=e;if(!t||t==="--help"||t==="help"||t==="-h")return{command:"help"};if(t!=="generate")throw new Error(`tanstack-fetch: unknown command "${t}"`);let r=new Map;for(let a=0;a<n.length;a+=1){let m=n[a];if(!m.startsWith("--"))continue;let c=m.slice(2),l=n[a+1];if(!l||l.startsWith("--"))throw new Error(`tanstack-fetch: missing value for --${c}`);r.set(c,l),a+=1}let s=r.get("spec"),o=r.get("out");if(!s||!o)throw new Error("tanstack-fetch: generate requires --spec and --out");return{command:"generate",spec:s,out:o}},w=`tanstack-fetch
232
33
 
233
34
  Usage:
234
35
  tanstack-fetch generate --spec ./openapi.json --out ./src/api
@@ -236,22 +37,4 @@ Usage:
236
37
  Flags:
237
38
  --spec OpenAPI/Swagger JSON or YAML file
238
39
  --out Directory for generated client files
239
- `;
240
-
241
- // src/cli/index.ts
242
- var runCli = async (argv = process.argv.slice(2)) => {
243
- const args = parseArgs(argv);
244
- if (args.command === "help") {
245
- console.log(helpText);
246
- return;
247
- }
248
- const spec = await loadSpec(args.spec);
249
- await generateClient(spec, args.out);
250
- console.log(`tanstack-fetch: generated client in ${args.out}`);
251
- };
252
- runCli().catch((error) => {
253
- const message = error instanceof Error ? error.message : "Unknown CLI error";
254
- console.error(message);
255
- process.exitCode = 1;
256
- });
257
- //# sourceMappingURL=cli.js.map
40
+ `;var P=async(e=process.argv.slice(2))=>{let t=S(e);if(t.command==="help"){console.log(w);return}let n=await $(t.spec);await O(n,t.out),console.log(`tanstack-fetch: generated client in ${t.out}`)};P().catch(e=>{let t=e instanceof Error?e.message:"Unknown CLI error";console.error(t),process.exitCode=1});
@@ -0,0 +1,156 @@
1
+ import { M as MaybePromise, C as ClientSource, I as IncomingHeaders, b as HttpInterceptor, P as PluginName, A as AuthConfig, S as StatusHandler, g as StatusHandlers, c as HttpMethod, h as PathParams, Q as QueryParams, a as FetchErrorInfo, F as FetchResult } from './config.type-eG_cuxpu.cjs';
2
+
3
+ type SseEvent<T = unknown> = {
4
+ event?: string;
5
+ data: T;
6
+ id?: string;
7
+ retry?: number;
8
+ };
9
+ type SseSubscription = {
10
+ /** Stop the stream. */
11
+ close: () => void;
12
+ };
13
+ type SseHandlers<T = unknown> = {
14
+ /** Simple path — only the payload. */
15
+ onMessage?: (data: T, event: SseEvent<T>) => void;
16
+ /** Full SSE event (`event`, `data`, `id`). */
17
+ onEvent?: (event: SseEvent<T>) => void;
18
+ onOpen?: () => void;
19
+ onError?: (error: unknown) => void;
20
+ onClose?: () => void;
21
+ };
22
+
23
+ type UploadProgressEvent = {
24
+ loaded: number;
25
+ total?: number;
26
+ /** `loaded / total` when `total` is known (0–1). */
27
+ progress?: number;
28
+ };
29
+ type UploadProgressHandler = (event: UploadProgressEvent) => void;
30
+ type FormDataPrimitive = string | number | boolean | Blob;
31
+ type FormDataFieldValue = FormDataPrimitive | null | undefined | FormDataPrimitive[];
32
+ type FormDataFields = Record<string, FormDataFieldValue>;
33
+ type UploadBody = FormData | Blob | ArrayBuffer | URLSearchParams | string;
34
+ type UploadOptions = {
35
+ /** Pre-built body (`FormData`, `Blob`, `File`, …). */
36
+ body?: UploadBody;
37
+ /** Single file — appended as `fieldName` (default `"file"`). */
38
+ file?: Blob;
39
+ /** Multiple files — same field name repeated. */
40
+ files?: Blob[];
41
+ /** Extra multipart fields (strings, numbers, Blobs). */
42
+ fields?: FormDataFields;
43
+ /** Form field name for `file` / `files`. Default `"file"`. */
44
+ fieldName?: string;
45
+ /** HTTP method. Default `POST`. */
46
+ method?: 'POST' | 'PUT' | 'PATCH';
47
+ onUploadProgress?: UploadProgressHandler;
48
+ };
49
+
50
+ type RequestInterceptorConfig = {
51
+ use?: HttpInterceptor[];
52
+ eject?: string[];
53
+ };
54
+ type RequestOptions = {
55
+ params?: PathParams;
56
+ query?: QueryParams;
57
+ body?: unknown;
58
+ headers?: HeadersInit;
59
+ signal?: AbortSignal;
60
+ timeoutMs?: number;
61
+ /** Default `true` — matches TanStack Query `queryFn` (throw on HTTP error). */
62
+ throwOnError?: boolean;
63
+ parseAs?: 'json' | 'text' | 'blob';
64
+ operation?: string;
65
+ interceptors?: RequestInterceptorConfig;
66
+ /** Browser-only — uses XHR under the hood when set (fetch has no upload progress). */
67
+ onUploadProgress?: UploadProgressHandler;
68
+ };
69
+ type CreateFetchOptions = {
70
+ baseUrl?: string;
71
+ headers?: HeadersInit | (() => MaybePromise<HeadersInit>);
72
+ source?: ClientSource;
73
+ incoming?: IncomingHeaders | (() => MaybePromise<IncomingHeaders>);
74
+ timeoutMs?: number;
75
+ /** Default `true` for TanStack Query. Set `false` to get `FetchResult`. */
76
+ throwOnError?: boolean;
77
+ interceptors?: HttpInterceptor[];
78
+ plugins?: PluginName[];
79
+ fetch?: typeof fetch;
80
+ credentials?: RequestCredentials;
81
+ maxRetries?: number;
82
+ /** Simple auth: attach Bearer token on every request. */
83
+ getToken?: () => MaybePromise<string | null | undefined>;
84
+ /** Advanced auth config (overrides `getToken` when both set via `auth`). */
85
+ auth?: AuthConfig;
86
+ /** Called on HTTP 401 before the error is thrown / returned. */
87
+ onUnauthorized?: StatusHandler;
88
+ /** Called on HTTP 403. */
89
+ onForbidden?: StatusHandler;
90
+ /** Called on HTTP 404. */
91
+ onNotFound?: StatusHandler;
92
+ /** Called on HTTP 5xx (500–599). */
93
+ onServerError?: StatusHandler;
94
+ /** Advanced per-status map (`401`, `403`, `4xx`, `5xx`, `default`, …). */
95
+ onStatus?: StatusHandlers;
96
+ };
97
+ type ThrowingOptions = Omit<RequestOptions, 'throwOnError'> & {
98
+ throwOnError?: true;
99
+ };
100
+ type ResultOptions = Omit<RequestOptions, 'throwOnError'> & {
101
+ throwOnError: false;
102
+ };
103
+ type FetchMethod = {
104
+ <T>(path: string, options?: ThrowingOptions): Promise<T>;
105
+ <T, E = FetchErrorInfo>(path: string, options: ResultOptions): Promise<FetchResult<T, E>>;
106
+ };
107
+ type FetchRequest = {
108
+ <T>(method: HttpMethod, path: string, options?: ThrowingOptions): Promise<T>;
109
+ <T, E = FetchErrorInfo>(method: HttpMethod, path: string, options: ResultOptions): Promise<FetchResult<T, E>>;
110
+ };
111
+ type UploadCallOptions = Omit<RequestOptions, 'body' | 'onUploadProgress'> & UploadOptions;
112
+ type UploadMethod = {
113
+ <T>(path: string, options?: UploadCallOptions & {
114
+ throwOnError?: true;
115
+ }): Promise<T>;
116
+ <T, E = FetchErrorInfo>(path: string, options: UploadCallOptions & {
117
+ throwOnError: false;
118
+ }): Promise<FetchResult<T, E>>;
119
+ };
120
+ type SseCallOptions<T = unknown> = RequestOptions & SseHandlers<T> & {
121
+ lastEventId?: string;
122
+ };
123
+ type FetchClient = {
124
+ use: (name: string, interceptor: Omit<HttpInterceptor, 'name'> & {
125
+ name?: string;
126
+ }, config?: {
127
+ order?: number;
128
+ }) => void;
129
+ eject: (name: string) => void;
130
+ request: FetchRequest;
131
+ get: FetchMethod;
132
+ post: FetchMethod;
133
+ put: FetchMethod;
134
+ patch: FetchMethod;
135
+ delete: FetchMethod;
136
+ /** Multipart / file upload — returns the same typed response body as `post`. */
137
+ upload: UploadMethod;
138
+ /**
139
+ * Simple: pass `onMessage` / `onEvent` → returns `{ close }`.
140
+ * Advanced: no handlers → `AsyncIterable` for `for await`.
141
+ */
142
+ sse: {
143
+ <T>(path: string, options: SseCallOptions<T> & ({
144
+ onMessage: SseHandlers<T>['onMessage'];
145
+ } | {
146
+ onEvent: SseHandlers<T>['onEvent'];
147
+ })): SseSubscription;
148
+ <T>(path: string, options?: SseCallOptions<T>): AsyncIterable<SseEvent<T>>;
149
+ };
150
+ };
151
+ /** @deprecated Use CreateFetchOptions */
152
+ type CreateClientOptions = CreateFetchOptions;
153
+ /** @deprecated Use FetchClient */
154
+ type HttpClient = FetchClient;
155
+
156
+ export type { CreateFetchOptions as C, FetchClient as F, HttpClient as H, RequestOptions as R, SseCallOptions as S, UploadCallOptions as U, FormDataFields as a, CreateClientOptions as b, FormDataFieldValue as c, SseEvent as d, SseHandlers as e, SseSubscription as f, UploadOptions as g, UploadProgressEvent as h, UploadProgressHandler as i };