@richie-rpc/client 1.2.2 → 1.2.4
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 +300 -40
- package/dist/cjs/index.cjs +414 -6
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +415 -7
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +71 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ import { contract } from './contract';
|
|
|
19
19
|
const client = createClient(contract, {
|
|
20
20
|
baseUrl: 'https://api.example.com',
|
|
21
21
|
headers: {
|
|
22
|
-
|
|
23
|
-
}
|
|
22
|
+
Authorization: 'Bearer token123',
|
|
23
|
+
},
|
|
24
24
|
});
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -36,20 +36,22 @@ const client = createClient(contract, {
|
|
|
36
36
|
|
|
37
37
|
// Relative URL with path prefix (browser-friendly)
|
|
38
38
|
const client = createClient(contract, {
|
|
39
|
-
baseUrl: '/api',
|
|
39
|
+
baseUrl: '/api', // Resolves to current origin + /api
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
// Just the path (same origin)
|
|
43
43
|
const client = createClient(contract, {
|
|
44
|
-
baseUrl: '/',
|
|
44
|
+
baseUrl: '/', // Resolves to current origin
|
|
45
45
|
});
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
**How it works:**
|
|
49
|
+
|
|
49
50
|
- **Absolute URLs** (`http://...` or `https://...`): Used as-is
|
|
50
51
|
- **Relative URLs** (starting with `/`): Automatically resolved using `window.location.origin` in browsers, or `http://localhost` in non-browser environments
|
|
51
52
|
|
|
52
53
|
**Example:** In a browser at `https://example.com`, if your contract defines `/users`:
|
|
54
|
+
|
|
53
55
|
- With `baseUrl: '/api'` → actual URL is `https://example.com/api/users`
|
|
54
56
|
- With `baseUrl: '/'` → actual URL is `https://example.com/users`
|
|
55
57
|
|
|
@@ -59,8 +61,8 @@ The client provides fully typed methods for each endpoint in your contract:
|
|
|
59
61
|
|
|
60
62
|
```typescript
|
|
61
63
|
// GET request with path parameters
|
|
62
|
-
const user = await client.getUser({
|
|
63
|
-
params: { id: '123' }
|
|
64
|
+
const user = await client.getUser({
|
|
65
|
+
params: { id: '123' },
|
|
64
66
|
});
|
|
65
67
|
// user is typed based on the response schema
|
|
66
68
|
|
|
@@ -68,26 +70,59 @@ const user = await client.getUser({
|
|
|
68
70
|
const newUser = await client.createUser({
|
|
69
71
|
body: {
|
|
70
72
|
name: 'John Doe',
|
|
71
|
-
email: 'john@example.com'
|
|
72
|
-
}
|
|
73
|
+
email: 'john@example.com',
|
|
74
|
+
},
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
// Request with query parameters
|
|
76
78
|
const users = await client.listUsers({
|
|
77
79
|
query: {
|
|
78
80
|
limit: '10',
|
|
79
|
-
offset: '0'
|
|
80
|
-
}
|
|
81
|
+
offset: '0',
|
|
82
|
+
},
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
// Request with custom headers
|
|
84
86
|
const data = await client.getData({
|
|
85
87
|
headers: {
|
|
86
|
-
'X-Custom-Header': 'value'
|
|
87
|
-
}
|
|
88
|
+
'X-Custom-Header': 'value',
|
|
89
|
+
},
|
|
88
90
|
});
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
### File Uploads (multipart/form-data)
|
|
94
|
+
|
|
95
|
+
Upload files with full type safety. Files can be nested anywhere in the request body:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Contract defines the file upload endpoint
|
|
99
|
+
// (see @richie-rpc/core for defining contentType: 'multipart/form-data')
|
|
100
|
+
|
|
101
|
+
// Client usage - just pass File objects in the body
|
|
102
|
+
const file1 = new File(['content'], 'report.pdf', { type: 'application/pdf' });
|
|
103
|
+
const file2 = new File(['data'], 'data.csv', { type: 'text/csv' });
|
|
104
|
+
|
|
105
|
+
const response = await client.uploadDocuments({
|
|
106
|
+
body: {
|
|
107
|
+
documents: [
|
|
108
|
+
{ file: file1, name: 'Q4 Report', tags: ['quarterly', 'finance'] },
|
|
109
|
+
{ file: file2, name: 'Sales Data' },
|
|
110
|
+
],
|
|
111
|
+
category: 'reports',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (response.status === 201) {
|
|
116
|
+
console.log(`Uploaded ${response.data.uploadedCount} files`);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The client automatically:
|
|
121
|
+
|
|
122
|
+
- Detects `multipart/form-data` content type from the contract
|
|
123
|
+
- Serializes nested structures with File objects to FormData
|
|
124
|
+
- Sets the correct `Content-Type` header with boundary
|
|
125
|
+
|
|
91
126
|
### Canceling Requests
|
|
92
127
|
|
|
93
128
|
You can cancel in-flight requests using `AbortController`:
|
|
@@ -118,20 +153,23 @@ try {
|
|
|
118
153
|
```typescript
|
|
119
154
|
useEffect(() => {
|
|
120
155
|
const controller = new AbortController();
|
|
121
|
-
|
|
122
|
-
client
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
|
|
157
|
+
client
|
|
158
|
+
.getConversation({
|
|
159
|
+
params: { projectId, sessionId },
|
|
160
|
+
abortSignal: controller.signal,
|
|
161
|
+
})
|
|
162
|
+
.then((response) => {
|
|
163
|
+
if (response.status === 200) {
|
|
164
|
+
setData(response.data);
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
.catch((error) => {
|
|
168
|
+
if (error.name !== 'AbortError') {
|
|
169
|
+
console.error('Request failed:', error);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
135
173
|
// Cleanup: abort request if component unmounts
|
|
136
174
|
return () => controller.abort();
|
|
137
175
|
}, [projectId, sessionId]);
|
|
@@ -158,18 +196,241 @@ try {
|
|
|
158
196
|
}
|
|
159
197
|
```
|
|
160
198
|
|
|
199
|
+
## Streaming Responses
|
|
200
|
+
|
|
201
|
+
For streaming endpoints, the client returns an event-based result:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const contract = defineContract({
|
|
205
|
+
generateText: {
|
|
206
|
+
type: 'streaming',
|
|
207
|
+
method: 'POST',
|
|
208
|
+
path: '/generate',
|
|
209
|
+
body: z.object({ prompt: z.string() }),
|
|
210
|
+
chunk: z.object({ text: z.string() }),
|
|
211
|
+
finalResponse: z.object({ totalTokens: z.number() }),
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const client = createClient(contract, { baseUrl: 'http://localhost:3000' });
|
|
216
|
+
|
|
217
|
+
const result = client.generateText({ body: { prompt: 'Hello world' } });
|
|
218
|
+
|
|
219
|
+
// Listen for chunks
|
|
220
|
+
result.on('chunk', (chunk) => {
|
|
221
|
+
process.stdout.write(chunk.text);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Listen for stream completion
|
|
225
|
+
result.on('close', (final) => {
|
|
226
|
+
if (final) {
|
|
227
|
+
console.log(`\nTotal tokens: ${final.totalTokens}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Handle errors
|
|
232
|
+
result.on('error', (error) => {
|
|
233
|
+
console.error('Stream error:', error.message);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Abort if needed
|
|
237
|
+
result.abort();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### StreamingResult Interface
|
|
241
|
+
|
|
242
|
+
- `on('chunk', handler)` - Subscribe to chunks
|
|
243
|
+
- `on('close', handler)` - Subscribe to stream close (with optional final response)
|
|
244
|
+
- `on('error', handler)` - Subscribe to errors
|
|
245
|
+
- `abort()` - Abort the stream
|
|
246
|
+
- `aborted` - Check if the stream was aborted
|
|
247
|
+
|
|
248
|
+
## Server-Sent Events (SSE)
|
|
249
|
+
|
|
250
|
+
For SSE endpoints, the client returns an event-based connection:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const contract = defineContract({
|
|
254
|
+
notifications: {
|
|
255
|
+
type: 'sse',
|
|
256
|
+
method: 'GET',
|
|
257
|
+
path: '/notifications',
|
|
258
|
+
events: {
|
|
259
|
+
message: z.object({ text: z.string(), timestamp: z.string() }),
|
|
260
|
+
heartbeat: z.object({ timestamp: z.string() }),
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const client = createClient(contract, { baseUrl: 'http://localhost:3000' });
|
|
266
|
+
|
|
267
|
+
const conn = client.notifications();
|
|
268
|
+
|
|
269
|
+
// Listen for specific event types
|
|
270
|
+
conn.on('message', (data) => {
|
|
271
|
+
console.log(`Message: ${data.text} at ${data.timestamp}`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
conn.on('heartbeat', (data) => {
|
|
275
|
+
console.log('Heartbeat:', data.timestamp);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Handle connection errors
|
|
279
|
+
conn.on('error', (error) => {
|
|
280
|
+
console.error('SSE error:', error.message);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Close when done
|
|
284
|
+
conn.close();
|
|
285
|
+
|
|
286
|
+
// Check connection state
|
|
287
|
+
console.log('State:', conn.state); // 'connecting' | 'open' | 'closed'
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### SSEConnection Interface
|
|
291
|
+
|
|
292
|
+
- `on(event, handler)` - Subscribe to a specific event type
|
|
293
|
+
- `on('error', handler)` - Subscribe to connection errors
|
|
294
|
+
- `close()` - Close the connection
|
|
295
|
+
- `state` - Current connection state
|
|
296
|
+
|
|
297
|
+
## WebSocket Client
|
|
298
|
+
|
|
299
|
+
For bidirectional real-time communication, use `createWebSocketClient`:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { createWebSocketClient } from '@richie-rpc/client';
|
|
303
|
+
import { defineWebSocketContract } from '@richie-rpc/core';
|
|
304
|
+
|
|
305
|
+
const wsContract = defineWebSocketContract({
|
|
306
|
+
chat: {
|
|
307
|
+
path: '/ws/chat/:roomId',
|
|
308
|
+
params: z.object({ roomId: z.string() }),
|
|
309
|
+
clientMessages: {
|
|
310
|
+
sendMessage: { payload: z.object({ text: z.string() }) },
|
|
311
|
+
},
|
|
312
|
+
serverMessages: {
|
|
313
|
+
message: { payload: z.object({ userId: z.string(), text: z.string() }) },
|
|
314
|
+
error: { payload: z.object({ code: z.string(), message: z.string() }) },
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const wsClient = createWebSocketClient(wsContract, {
|
|
320
|
+
baseUrl: 'ws://localhost:3000',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Get a typed WebSocket instance
|
|
324
|
+
const chat = wsClient.chat({ params: { roomId: 'general' } });
|
|
325
|
+
|
|
326
|
+
// Connect (returns disconnect function)
|
|
327
|
+
const disconnect = chat.connect();
|
|
328
|
+
|
|
329
|
+
// Track connection state
|
|
330
|
+
chat.onStateChange((connected) => {
|
|
331
|
+
console.log('Connected:', connected);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Listen for specific message types
|
|
335
|
+
chat.on('message', (payload) => {
|
|
336
|
+
console.log(`${payload.userId}: ${payload.text}`);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
chat.on('error', (payload) => {
|
|
340
|
+
console.error(`Error ${payload.code}: ${payload.message}`);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Listen for all messages
|
|
344
|
+
chat.onMessage((message) => {
|
|
345
|
+
console.log('Received:', message.type, message.payload);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Handle connection errors
|
|
349
|
+
chat.onError((error) => {
|
|
350
|
+
console.error('Connection error:', error.message);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Send messages (validates before sending)
|
|
354
|
+
chat.send('sendMessage', { text: 'Hello!' });
|
|
355
|
+
|
|
356
|
+
// Disconnect when done
|
|
357
|
+
disconnect();
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### TypedWebSocket Interface
|
|
361
|
+
|
|
362
|
+
- `connect()` - Connect to the WebSocket server, returns disconnect function
|
|
363
|
+
- `send(type, payload)` - Send a typed message (validates before sending)
|
|
364
|
+
- `on(type, handler)` - Subscribe to a specific message type
|
|
365
|
+
- `onMessage(handler)` - Subscribe to all messages
|
|
366
|
+
- `onStateChange(handler)` - Track connection state changes
|
|
367
|
+
- `onError(handler)` - Handle connection errors
|
|
368
|
+
- `connected` - Check current connection state
|
|
369
|
+
|
|
370
|
+
### React Integration Example
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
374
|
+
const [connected, setConnected] = useState(false);
|
|
375
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
376
|
+
|
|
377
|
+
const chat = useMemo(
|
|
378
|
+
() => wsClient.chat({ params: { roomId } }),
|
|
379
|
+
[roomId]
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Connection lifecycle
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
const disconnect = chat.connect();
|
|
385
|
+
return () => disconnect();
|
|
386
|
+
}, [chat]);
|
|
387
|
+
|
|
388
|
+
// Track connection state
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
return chat.onStateChange(setConnected);
|
|
391
|
+
}, [chat]);
|
|
392
|
+
|
|
393
|
+
// Subscribe to messages (only when connected)
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (!connected) return;
|
|
396
|
+
return chat.on('message', (payload) => {
|
|
397
|
+
setMessages((prev) => [...prev, payload]);
|
|
398
|
+
});
|
|
399
|
+
}, [connected, chat]);
|
|
400
|
+
|
|
401
|
+
const handleSend = (text: string) => {
|
|
402
|
+
chat.send('sendMessage', { text });
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<div>
|
|
407
|
+
<div>Status: {connected ? 'Connected' : 'Disconnected'}</div>
|
|
408
|
+
{messages.map((msg, i) => (
|
|
409
|
+
<div key={i}>{msg.userId}: {msg.text}</div>
|
|
410
|
+
))}
|
|
411
|
+
<button onClick={() => handleSend('Hello!')}>Send</button>
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
161
417
|
## Features
|
|
162
418
|
|
|
163
419
|
- ✅ Full type safety based on contract
|
|
164
420
|
- ✅ Automatic path parameter interpolation
|
|
165
421
|
- ✅ Query parameter encoding
|
|
166
422
|
- ✅ BasePath support in baseUrl
|
|
423
|
+
- ✅ HTTP Streaming with event-based API
|
|
424
|
+
- ✅ Server-Sent Events (SSE) client
|
|
425
|
+
- ✅ WebSocket client with typed messages
|
|
167
426
|
- ✅ Request validation before sending
|
|
168
427
|
- ✅ Response validation after receiving
|
|
169
428
|
- ✅ Detailed error information
|
|
170
429
|
- ✅ Support for all HTTP methods
|
|
171
430
|
- ✅ Custom headers per request
|
|
172
431
|
- ✅ Request cancellation with AbortController
|
|
432
|
+
- ✅ File uploads with `multipart/form-data`
|
|
433
|
+
- ✅ Nested file structures in request bodies
|
|
173
434
|
|
|
174
435
|
## Configuration
|
|
175
436
|
|
|
@@ -177,10 +438,10 @@ try {
|
|
|
177
438
|
|
|
178
439
|
```typescript
|
|
179
440
|
interface ClientConfig {
|
|
180
|
-
baseUrl: string;
|
|
181
|
-
headers?: Record<string, string>;
|
|
182
|
-
validateRequest?: boolean;
|
|
183
|
-
validateResponse?: boolean;
|
|
441
|
+
baseUrl: string; // Base URL for all requests
|
|
442
|
+
headers?: Record<string, string>; // Default headers
|
|
443
|
+
validateRequest?: boolean; // Validate before sending (default: true)
|
|
444
|
+
validateResponse?: boolean; // Validate after receiving (default: true)
|
|
184
445
|
}
|
|
185
446
|
```
|
|
186
447
|
|
|
@@ -192,7 +453,7 @@ Responses include both the status code and data:
|
|
|
192
453
|
const response = await client.getUser({ params: { id: '123' } });
|
|
193
454
|
|
|
194
455
|
console.log(response.status); // 200, 404, etc.
|
|
195
|
-
console.log(response.data);
|
|
456
|
+
console.log(response.data); // Typed response body
|
|
196
457
|
```
|
|
197
458
|
|
|
198
459
|
## Error Handling
|
|
@@ -206,11 +467,11 @@ Thrown when request data fails validation:
|
|
|
206
467
|
```typescript
|
|
207
468
|
try {
|
|
208
469
|
await client.createUser({
|
|
209
|
-
body: { email: 'invalid-email' }
|
|
470
|
+
body: { email: 'invalid-email' },
|
|
210
471
|
});
|
|
211
472
|
} catch (error) {
|
|
212
473
|
if (error instanceof ClientValidationError) {
|
|
213
|
-
console.log(error.field);
|
|
474
|
+
console.log(error.field); // 'body'
|
|
214
475
|
console.log(error.issues); // Zod validation issues
|
|
215
476
|
}
|
|
216
477
|
}
|
|
@@ -225,9 +486,9 @@ try {
|
|
|
225
486
|
await client.getUser({ params: { id: '999' } });
|
|
226
487
|
} catch (error) {
|
|
227
488
|
if (error instanceof HTTPError) {
|
|
228
|
-
console.log(error.status);
|
|
489
|
+
console.log(error.status); // 404
|
|
229
490
|
console.log(error.statusText); // 'Not Found'
|
|
230
|
-
console.log(error.body);
|
|
491
|
+
console.log(error.body); // Response body
|
|
231
492
|
}
|
|
232
493
|
}
|
|
233
494
|
```
|
|
@@ -239,12 +500,12 @@ All client methods are fully typed based on your contract:
|
|
|
239
500
|
```typescript
|
|
240
501
|
// ✅ Type-safe: required fields
|
|
241
502
|
await client.createUser({
|
|
242
|
-
body: { name: 'John', email: 'john@example.com' }
|
|
503
|
+
body: { name: 'John', email: 'john@example.com' },
|
|
243
504
|
});
|
|
244
505
|
|
|
245
506
|
// ❌ Type error: missing required field
|
|
246
507
|
await client.createUser({
|
|
247
|
-
body: { name: 'John' }
|
|
508
|
+
body: { name: 'John' },
|
|
248
509
|
});
|
|
249
510
|
|
|
250
511
|
// ✅ Type-safe: response data
|
|
@@ -279,8 +540,8 @@ You can disable validation:
|
|
|
279
540
|
```typescript
|
|
280
541
|
const client = createClient(contract, {
|
|
281
542
|
baseUrl: 'https://api.example.com',
|
|
282
|
-
validateRequest: false,
|
|
283
|
-
validateResponse: false
|
|
543
|
+
validateRequest: false, // Skip request validation
|
|
544
|
+
validateResponse: false, // Skip response validation
|
|
284
545
|
});
|
|
285
546
|
```
|
|
286
547
|
|
|
@@ -292,4 +553,3 @@ const client = createClient(contract, {
|
|
|
292
553
|
## License
|
|
293
554
|
|
|
294
555
|
MIT
|
|
295
|
-
|