@richie-rpc/client 1.2.1 → 1.2.3

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
@@ -88,6 +88,108 @@ const data = await client.getData({
88
88
  });
89
89
  ```
90
90
 
91
+ ### File Uploads (multipart/form-data)
92
+
93
+ Upload files with full type safety. Files can be nested anywhere in the request body:
94
+
95
+ ```typescript
96
+ // Contract defines the file upload endpoint
97
+ // (see @richie-rpc/core for defining contentType: 'multipart/form-data')
98
+
99
+ // Client usage - just pass File objects in the body
100
+ const file1 = new File(['content'], 'report.pdf', { type: 'application/pdf' });
101
+ const file2 = new File(['data'], 'data.csv', { type: 'text/csv' });
102
+
103
+ const response = await client.uploadDocuments({
104
+ body: {
105
+ documents: [
106
+ { file: file1, name: 'Q4 Report', tags: ['quarterly', 'finance'] },
107
+ { file: file2, name: 'Sales Data' },
108
+ ],
109
+ category: 'reports',
110
+ },
111
+ });
112
+
113
+ if (response.status === 201) {
114
+ console.log(`Uploaded ${response.data.uploadedCount} files`);
115
+ }
116
+ ```
117
+
118
+ The client automatically:
119
+ - Detects `multipart/form-data` content type from the contract
120
+ - Serializes nested structures with File objects to FormData
121
+ - Sets the correct `Content-Type` header with boundary
122
+
123
+ ### Canceling Requests
124
+
125
+ You can cancel in-flight requests using `AbortController`:
126
+
127
+ ```typescript
128
+ const controller = new AbortController();
129
+
130
+ // Pass the abort signal to the request
131
+ const promise = client.getUser({
132
+ params: { id: '123' },
133
+ abortSignal: controller.signal,
134
+ });
135
+
136
+ // Cancel the request
137
+ controller.abort();
138
+
139
+ try {
140
+ await promise;
141
+ } catch (error) {
142
+ if (error.name === 'AbortError') {
143
+ console.log('Request was cancelled');
144
+ }
145
+ }
146
+ ```
147
+
148
+ **React Example:**
149
+
150
+ ```typescript
151
+ useEffect(() => {
152
+ const controller = new AbortController();
153
+
154
+ client.getConversation({
155
+ params: { projectId, sessionId },
156
+ abortSignal: controller.signal,
157
+ }).then((response) => {
158
+ if (response.status === 200) {
159
+ setData(response.data);
160
+ }
161
+ }).catch((error) => {
162
+ if (error.name !== 'AbortError') {
163
+ console.error('Request failed:', error);
164
+ }
165
+ });
166
+
167
+ // Cleanup: abort request if component unmounts
168
+ return () => controller.abort();
169
+ }, [projectId, sessionId]);
170
+ ```
171
+
172
+ **Timeout Example:**
173
+
174
+ ```typescript
175
+ const controller = new AbortController();
176
+
177
+ // Abort after 5 seconds
178
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
179
+
180
+ try {
181
+ const response = await client.getData({
182
+ abortSignal: controller.signal,
183
+ });
184
+ clearTimeout(timeoutId);
185
+ console.log(response.data);
186
+ } catch (error) {
187
+ if (error.name === 'AbortError') {
188
+ console.log('Request timed out');
189
+ }
190
+ }
191
+ ```
192
+
91
193
  ## Features
92
194
 
93
195
  - ✅ Full type safety based on contract
@@ -99,6 +201,9 @@ const data = await client.getData({
99
201
  - ✅ Detailed error information
100
202
  - ✅ Support for all HTTP methods
101
203
  - ✅ Custom headers per request
204
+ - ✅ Request cancellation with AbortController
205
+ - ✅ File uploads with `multipart/form-data`
206
+ - ✅ Nested file structures in request bodies
102
207
 
103
208
  ## Configuration
104
209
 
@@ -192,8 +297,9 @@ Each client method accepts an options object with the following fields (based on
192
297
  - `query`: Query parameters (if endpoint has query schema)
193
298
  - `headers`: Custom headers (if endpoint has headers schema)
194
299
  - `body`: Request body (if endpoint has body schema)
300
+ - `abortSignal`: AbortSignal for request cancellation (optional, always available)
195
301
 
196
- Only the fields defined in the contract are available and typed.
302
+ Only the fields defined in the contract are available and typed (except `abortSignal`, which is always available).
197
303
 
198
304
  ## Validation
199
305
 
@@ -115,9 +115,17 @@ async function makeRequest(config, endpoint, options) {
115
115
  method: endpoint.method,
116
116
  headers
117
117
  };
118
+ if (options.abortSignal) {
119
+ init.signal = options.abortSignal;
120
+ }
118
121
  if (options.body !== undefined) {
119
- headers.set("content-type", "application/json");
120
- init.body = JSON.stringify(options.body);
122
+ const contentType = endpoint.contentType ?? "application/json";
123
+ if (contentType === "multipart/form-data") {
124
+ init.body = import_core.objectToFormData(options.body);
125
+ } else {
126
+ headers.set("content-type", "application/json");
127
+ init.body = JSON.stringify(options.body);
128
+ }
121
129
  }
122
130
  const response = await fetch(url, init);
123
131
  let data;
@@ -184,4 +192,4 @@ function createTypedClient(_config) {
184
192
  }
185
193
  })
186
194
 
187
- //# debugId=9A3FBD317C0CACC164756E2164756E21
195
+ //# debugId=DDB0DF58823434B764756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../index.ts"],
4
4
  "sourcesContent": [
5
- "import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add body if present\n if (options.body !== undefined) {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Resolve relative baseUrl to absolute URL in browser contexts\n */\nfunction resolveBaseUrl(baseUrl: string): string {\n // If baseUrl is already absolute, return as-is\n if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {\n return baseUrl;\n }\n\n // If baseUrl is relative (starts with /), resolve it using window.location in browser\n if (baseUrl.startsWith('/')) {\n const g = globalThis as unknown as { location?: { origin?: string } };\n const origin = g?.location?.origin || 'http://localhost';\n return origin + baseUrl;\n }\n\n // Otherwise, assume it's a full URL\n return baseUrl;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n // Resolve relative baseUrl to absolute URL\n const resolvedConfig = {\n ...config,\n baseUrl: resolveBaseUrl(config.baseUrl),\n };\n\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(resolvedConfig, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
5
+ "import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath, objectToFormData } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n abortSignal?: AbortSignal;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add abort signal if present\n if (options.abortSignal) {\n init.signal = options.abortSignal;\n }\n\n // Add body if present\n if (options.body !== undefined) {\n const contentType = endpoint.contentType ?? 'application/json';\n\n if (contentType === 'multipart/form-data') {\n // Don't set Content-Type header - browser sets boundary automatically\n init.body = objectToFormData(options.body as Record<string, unknown>);\n } else {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Resolve relative baseUrl to absolute URL in browser contexts\n */\nfunction resolveBaseUrl(baseUrl: string): string {\n // If baseUrl is already absolute, return as-is\n if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {\n return baseUrl;\n }\n\n // If baseUrl is relative (starts with /), resolve it using window.location in browser\n if (baseUrl.startsWith('/')) {\n const g = globalThis as unknown as { location?: { origin?: string } };\n const origin = g?.location?.origin || 'http://localhost';\n return origin + baseUrl;\n }\n\n // Otherwise, assume it's a full URL\n return baseUrl;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n // Resolve relative baseUrl to absolute URL\n const resolvedConfig = {\n ...config,\n baseUrl: resolveBaseUrl(config.baseUrl),\n };\n\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(resolvedConfig, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "9A3FBD317C0CACC164756E2164756E21",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQ4D,IAA5D;AAAA;AAuCO,MAAM,8BAA8B,MAAM;AAAA,EAEtC;AAAA,EACA;AAAA,EAFT,WAAW,CACF,OACA,QACP;AAAA,IACA,MAAM,yBAAyB,OAAO;AAAA,IAH/B;AAAA,IACA;AAAA,IAGP,KAAK,OAAO;AAAA;AAEhB;AAAA;AAGO,MAAM,kBAAkB,MAAM;AAAA,EAE1B;AAAA,EACA;AAAA,EACA;AAAA,EAHT,WAAW,CACF,QACA,YACA,MACP;AAAA,IACA,MAAM,cAAc,WAAW,YAAY;AAAA,IAJpC;AAAA,IACA;AAAA,IACA;AAAA,IAGP,KAAK,OAAO;AAAA;AAEhB;AAKA,SAAS,eAA6C,CACpD,UACA,SACM;AAAA,EAEN,IAAI,SAAS,UAAU,QAAQ,QAAQ;AAAA,IACrC,MAAM,SAAS,SAAS,OAAO,UAAU,QAAQ,MAAM;AAAA,IACvD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,UAAU,OAAO,MAAM,MAAM;AAAA,IAC/D;AAAA,EACF;AAAA,EAGA,IAAI,SAAS,SAAS,QAAQ,OAAO;AAAA,IACnC,MAAM,SAAS,SAAS,MAAM,UAAU,QAAQ,KAAK;AAAA,IACrD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,SAAS,OAAO,MAAM,MAAM;AAAA,IAC9D;AAAA,EACF;AAAA,EAGA,IAAI,SAAS,WAAW,QAAQ,SAAS;AAAA,IACvC,MAAM,SAAS,SAAS,QAAQ,UAAU,QAAQ,OAAO;AAAA,IACzD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,WAAW,OAAO,MAAM,MAAM;AAAA,IAChE;AAAA,EACF;AAAA,EAGA,IAAI,SAAS,QAAQ,QAAQ,MAAM;AAAA,IACjC,MAAM,SAAS,SAAS,KAAK,UAAU,QAAQ,IAAI;AAAA,IACnD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,QAAQ,OAAO,MAAM,MAAM;AAAA,IAC7D;AAAA,EACF;AAAA;AAMF,SAAS,gBAA8C,CACrD,UACA,QACA,MACM;AAAA,EACN,MAAM,iBAAiB,SAAS,UAAU;AAAA,EAC1C,IAAI,gBAAgB;AAAA,IAClB,MAAM,SAAS,eAAe,UAAU,IAAI;AAAA,IAC5C,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,YAAY,WAAW,OAAO,MAAM,MAAM;AAAA,IAC5E;AAAA,EACF;AAAA;AAMF,eAAe,WAAyC,CACtD,QACA,UACA,SAC8B;AAAA,EAE9B,IAAI,OAAO,oBAAoB,OAAO;AAAA,IACpC,gBAAgB,UAAU,OAAO;AAAA,EACnC;AAAA,EAGA,IAAI,OAAO,SAAS;AAAA,EACpB,IAAI,QAAQ,QAAQ;AAAA,IAClB,OAAO,4BAAgB,MAAM,QAAQ,MAAyC;AAAA,EAChF;AAAA,EAEA,MAAM,MAAM,qBACV,OAAO,SACP,MACA,QAAQ,KACV;AAAA,EAGA,MAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAAA,EAC1C,IAAI,QAAQ,SAAS;AAAA,IACnB,YAAY,KAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAAA,MAC1D,QAAQ,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IAChC;AAAA,EACF;AAAA,EAGA,MAAM,OAAoB;AAAA,IACxB,QAAQ,SAAS;AAAA,IACjB;AAAA,EACF;AAAA,EAGA,IAAI,QAAQ,aAAa;AAAA,IACvB,KAAK,SAAS,QAAQ;AAAA,EACxB;AAAA,EAGA,IAAI,QAAQ,SAAS,WAAW;AAAA,IAC9B,MAAM,cAAc,SAAS,eAAe;AAAA,IAE5C,IAAI,gBAAgB,uBAAuB;AAAA,MAEzC,KAAK,OAAO,6BAAiB,QAAQ,IAA+B;AAAA,IACtE,EAAO;AAAA,MACL,QAAQ,IAAI,gBAAgB,kBAAkB;AAAA,MAC9C,KAAK,OAAO,KAAK,UAAU,QAAQ,IAAI;AAAA;AAAA,EAE3C;AAAA,EAGA,MAAM,WAAW,MAAM,MAAM,KAAK,IAAI;AAAA,EAGtC,IAAI;AAAA,EAGJ,IAAI,SAAS,WAAW,KAAK;AAAA,IAC3B,OAAO,CAAC;AAAA,EACV,EAAO;AAAA,IACL,MAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAAA,IAE5D,IAAI,YAAY,SAAS,kBAAkB,GAAG;AAAA,MAC5C,OAAO,MAAM,SAAS,KAAK;AAAA,IAC7B,EAAO,SAAI,YAAY,SAAS,OAAO,GAAG;AAAA,MACxC,OAAO,MAAM,SAAS,KAAK;AAAA,IAC7B,EAAO;AAAA,MAEL,MAAM,OAAO,MAAM,SAAS,KAAK;AAAA,MACjC,IAAI,MAAM;AAAA,QACR,OAAO;AAAA,MACT,EAAO;AAAA,QACL,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA,EAMd,IAAI,CAAC,SAAS,MAAM,EAAE,SAAS,UAAU,SAAS,YAAY;AAAA,IAC5D,MAAM,IAAI,UAAU,SAAS,QAAQ,SAAS,YAAY,IAAI;AAAA,EAChE;AAAA,EAGA,IAAI,OAAO,qBAAqB,OAAO;AAAA,IACrC,iBAAiB,UAAU,SAAS,QAAQ,IAAI;AAAA,EAClD;AAAA,EAEA,OAAO;AAAA,IACL,QAAQ,SAAS;AAAA,IACjB;AAAA,EACF;AAAA;AAMF,SAAS,cAAc,CAAC,SAAyB;AAAA,EAE/C,IAAI,QAAQ,WAAW,SAAS,KAAK,QAAQ,WAAW,UAAU,GAAG;AAAA,IACnE,OAAO;AAAA,EACT;AAAA,EAGA,IAAI,QAAQ,WAAW,GAAG,GAAG;AAAA,IAC3B,MAAM,IAAI;AAAA,IACV,MAAM,SAAS,GAAG,UAAU,UAAU;AAAA,IACtC,OAAO,SAAS;AAAA,EAClB;AAAA,EAGA,OAAO;AAAA;AAMF,SAAS,YAAgC,CAAC,UAAa,QAAiC;AAAA,EAE7F,MAAM,iBAAiB;AAAA,OAClB;AAAA,IACH,SAAS,eAAe,OAAO,OAAO;AAAA,EACxC;AAAA,EAEA,MAAM,SAAkC,CAAC;AAAA,EAEzC,YAAY,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAAG;AAAA,IACvD,OAAO,QAAQ,CAAC,UAAsD,CAAC,MAAM;AAAA,MAC3E,OAAO,YAAY,gBAAgB,UAAU,OAAO;AAAA;AAAA,EAExD;AAAA,EAEA,OAAO;AAAA;AAOF,SAAS,iBAAqC,CAAC,SAAkC;AAAA,EACtF,OAAO,IAAI,MAAM,CAAC,GAAgB;AAAA,IAChC,GAAG,CAAC,SAAS,OAAe;AAAA,MAC1B,OAAO,OAAO,WAAuD,CAAC,MAAM;AAAA,QAG1E,MAAM,IAAI,MACR,0FACF;AAAA;AAAA;AAAA,EAGN,CAAC;AAAA;",
8
+ "debugId": "DDB0DF58823434B764756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@richie-rpc/client",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "type": "commonjs"
5
5
  }
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // packages/client/index.ts
3
- import { buildUrl, interpolatePath } from "@richie-rpc/core";
3
+ import { buildUrl, interpolatePath, objectToFormData } from "@richie-rpc/core";
4
4
 
5
5
  class ClientValidationError extends Error {
6
6
  field;
@@ -79,9 +79,17 @@ async function makeRequest(config, endpoint, options) {
79
79
  method: endpoint.method,
80
80
  headers
81
81
  };
82
+ if (options.abortSignal) {
83
+ init.signal = options.abortSignal;
84
+ }
82
85
  if (options.body !== undefined) {
83
- headers.set("content-type", "application/json");
84
- init.body = JSON.stringify(options.body);
86
+ const contentType = endpoint.contentType ?? "application/json";
87
+ if (contentType === "multipart/form-data") {
88
+ init.body = objectToFormData(options.body);
89
+ } else {
90
+ headers.set("content-type", "application/json");
91
+ init.body = JSON.stringify(options.body);
92
+ }
85
93
  }
86
94
  const response = await fetch(url, init);
87
95
  let data;
@@ -153,4 +161,4 @@ export {
153
161
  ClientValidationError
154
162
  };
155
163
 
156
- //# debugId=C61585053D4C236864756E2164756E21
164
+ //# debugId=B663AC78F4A6E33F64756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../index.ts"],
4
4
  "sourcesContent": [
5
- "import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add body if present\n if (options.body !== undefined) {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Resolve relative baseUrl to absolute URL in browser contexts\n */\nfunction resolveBaseUrl(baseUrl: string): string {\n // If baseUrl is already absolute, return as-is\n if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {\n return baseUrl;\n }\n\n // If baseUrl is relative (starts with /), resolve it using window.location in browser\n if (baseUrl.startsWith('/')) {\n const g = globalThis as unknown as { location?: { origin?: string } };\n const origin = g?.location?.origin || 'http://localhost';\n return origin + baseUrl;\n }\n\n // Otherwise, assume it's a full URL\n return baseUrl;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n // Resolve relative baseUrl to absolute URL\n const resolvedConfig = {\n ...config,\n baseUrl: resolveBaseUrl(config.baseUrl),\n };\n\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(resolvedConfig, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
5
+ "import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath, objectToFormData } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n abortSignal?: AbortSignal;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add abort signal if present\n if (options.abortSignal) {\n init.signal = options.abortSignal;\n }\n\n // Add body if present\n if (options.body !== undefined) {\n const contentType = endpoint.contentType ?? 'application/json';\n\n if (contentType === 'multipart/form-data') {\n // Don't set Content-Type header - browser sets boundary automatically\n init.body = objectToFormData(options.body as Record<string, unknown>);\n } else {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Resolve relative baseUrl to absolute URL in browser contexts\n */\nfunction resolveBaseUrl(baseUrl: string): string {\n // If baseUrl is already absolute, return as-is\n if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {\n return baseUrl;\n }\n\n // If baseUrl is relative (starts with /), resolve it using window.location in browser\n if (baseUrl.startsWith('/')) {\n const g = globalThis as unknown as { location?: { origin?: string } };\n const origin = g?.location?.origin || 'http://localhost';\n return origin + baseUrl;\n }\n\n // Otherwise, assume it's a full URL\n return baseUrl;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n // Resolve relative baseUrl to absolute URL\n const resolvedConfig = {\n ...config,\n baseUrl: resolveBaseUrl(config.baseUrl),\n };\n\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(resolvedConfig, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "C61585053D4C236864756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "B663AC78F4A6E33F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@richie-rpc/client",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "type": "module"
5
5
  }
@@ -11,6 +11,7 @@ export type EndpointRequestOptions<T extends EndpointDefinition> = {
11
11
  query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;
12
12
  headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;
13
13
  body?: ExtractBody<T> extends never ? never : ExtractBody<T>;
14
+ abortSignal?: AbortSignal;
14
15
  };
15
16
  export type EndpointResponse<T extends EndpointDefinition> = {
16
17
  [Status in keyof T['responses']]: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richie-rpc/client",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "main": "./dist/cjs/index.cjs",
5
5
  "exports": {
6
6
  ".": {
@@ -10,7 +10,7 @@
10
10
  }
11
11
  },
12
12
  "peerDependencies": {
13
- "@richie-rpc/core": "^1.2.1",
13
+ "@richie-rpc/core": "^1.2.2",
14
14
  "typescript": "^5",
15
15
  "zod": "^4.1.12"
16
16
  },