@richie-rpc/client 1.2.3 → 1.2.5
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 +266 -40
- package/dist/cjs/index.cjs +407 -4
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/websocket.cjs +178 -0
- package/dist/cjs/websocket.cjs.map +10 -0
- package/dist/mjs/index.mjs +407 -4
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/websocket.mjs +147 -0
- package/dist/mjs/websocket.mjs.map +10 -0
- package/dist/types/index.d.ts +71 -4
- package/dist/types/websocket.d.ts +97 -0
- 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,23 +70,23 @@ 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
|
|
|
@@ -116,6 +118,7 @@ if (response.status === 201) {
|
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
The client automatically:
|
|
121
|
+
|
|
119
122
|
- Detects `multipart/form-data` content type from the contract
|
|
120
123
|
- Serializes nested structures with File objects to FormData
|
|
121
124
|
- Sets the correct `Content-Type` header with boundary
|
|
@@ -150,20 +153,23 @@ try {
|
|
|
150
153
|
```typescript
|
|
151
154
|
useEffect(() => {
|
|
152
155
|
const controller = new AbortController();
|
|
153
|
-
|
|
154
|
-
client
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
|
|
167
173
|
// Cleanup: abort request if component unmounts
|
|
168
174
|
return () => controller.abort();
|
|
169
175
|
}, [projectId, sessionId]);
|
|
@@ -190,12 +196,233 @@ try {
|
|
|
190
196
|
}
|
|
191
197
|
```
|
|
192
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
|
+
|
|
193
417
|
## Features
|
|
194
418
|
|
|
195
419
|
- ✅ Full type safety based on contract
|
|
196
420
|
- ✅ Automatic path parameter interpolation
|
|
197
421
|
- ✅ Query parameter encoding
|
|
198
422
|
- ✅ BasePath support in baseUrl
|
|
423
|
+
- ✅ HTTP Streaming with event-based API
|
|
424
|
+
- ✅ Server-Sent Events (SSE) client
|
|
425
|
+
- ✅ WebSocket client with typed messages
|
|
199
426
|
- ✅ Request validation before sending
|
|
200
427
|
- ✅ Response validation after receiving
|
|
201
428
|
- ✅ Detailed error information
|
|
@@ -211,10 +438,10 @@ try {
|
|
|
211
438
|
|
|
212
439
|
```typescript
|
|
213
440
|
interface ClientConfig {
|
|
214
|
-
baseUrl: string;
|
|
215
|
-
headers?: Record<string, string>;
|
|
216
|
-
validateRequest?: boolean;
|
|
217
|
-
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)
|
|
218
445
|
}
|
|
219
446
|
```
|
|
220
447
|
|
|
@@ -226,7 +453,7 @@ Responses include both the status code and data:
|
|
|
226
453
|
const response = await client.getUser({ params: { id: '123' } });
|
|
227
454
|
|
|
228
455
|
console.log(response.status); // 200, 404, etc.
|
|
229
|
-
console.log(response.data);
|
|
456
|
+
console.log(response.data); // Typed response body
|
|
230
457
|
```
|
|
231
458
|
|
|
232
459
|
## Error Handling
|
|
@@ -240,11 +467,11 @@ Thrown when request data fails validation:
|
|
|
240
467
|
```typescript
|
|
241
468
|
try {
|
|
242
469
|
await client.createUser({
|
|
243
|
-
body: { email: 'invalid-email' }
|
|
470
|
+
body: { email: 'invalid-email' },
|
|
244
471
|
});
|
|
245
472
|
} catch (error) {
|
|
246
473
|
if (error instanceof ClientValidationError) {
|
|
247
|
-
console.log(error.field);
|
|
474
|
+
console.log(error.field); // 'body'
|
|
248
475
|
console.log(error.issues); // Zod validation issues
|
|
249
476
|
}
|
|
250
477
|
}
|
|
@@ -259,9 +486,9 @@ try {
|
|
|
259
486
|
await client.getUser({ params: { id: '999' } });
|
|
260
487
|
} catch (error) {
|
|
261
488
|
if (error instanceof HTTPError) {
|
|
262
|
-
console.log(error.status);
|
|
489
|
+
console.log(error.status); // 404
|
|
263
490
|
console.log(error.statusText); // 'Not Found'
|
|
264
|
-
console.log(error.body);
|
|
491
|
+
console.log(error.body); // Response body
|
|
265
492
|
}
|
|
266
493
|
}
|
|
267
494
|
```
|
|
@@ -273,12 +500,12 @@ All client methods are fully typed based on your contract:
|
|
|
273
500
|
```typescript
|
|
274
501
|
// ✅ Type-safe: required fields
|
|
275
502
|
await client.createUser({
|
|
276
|
-
body: { name: 'John', email: 'john@example.com' }
|
|
503
|
+
body: { name: 'John', email: 'john@example.com' },
|
|
277
504
|
});
|
|
278
505
|
|
|
279
506
|
// ❌ Type error: missing required field
|
|
280
507
|
await client.createUser({
|
|
281
|
-
body: { name: 'John' }
|
|
508
|
+
body: { name: 'John' },
|
|
282
509
|
});
|
|
283
510
|
|
|
284
511
|
// ✅ Type-safe: response data
|
|
@@ -313,8 +540,8 @@ You can disable validation:
|
|
|
313
540
|
```typescript
|
|
314
541
|
const client = createClient(contract, {
|
|
315
542
|
baseUrl: 'https://api.example.com',
|
|
316
|
-
validateRequest: false,
|
|
317
|
-
validateResponse: false
|
|
543
|
+
validateRequest: false, // Skip request validation
|
|
544
|
+
validateResponse: false, // Skip response validation
|
|
318
545
|
});
|
|
319
546
|
```
|
|
320
547
|
|
|
@@ -326,4 +553,3 @@ const client = createClient(contract, {
|
|
|
326
553
|
## License
|
|
327
554
|
|
|
328
555
|
MIT
|
|
329
|
-
|