@donkeylabs/server 0.1.3 → 0.1.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/examples/starter/node_modules/@donkeylabs/server/README.md +15 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/generate.ts +461 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/init.ts +476 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/interactive.ts +223 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/plugin.ts +192 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/donkeylabs +106 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/index.ts +100 -0
- package/examples/starter/node_modules/@donkeylabs/server/context.d.ts +17 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/api-client.md +520 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cache.md +437 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cli.md +353 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/core-services.md +338 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cron.md +465 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/errors.md +303 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/events.md +460 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/handlers.md +549 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/jobs.md +556 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/logger.md +316 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/middleware.md +682 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/plugins.md +524 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/project-structure.md +493 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/rate-limiter.md +525 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/router.md +566 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/sse.md +542 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/svelte-frontend.md +324 -0
- package/examples/starter/node_modules/@donkeylabs/server/mcp/donkeylabs-mcp +3238 -0
- package/examples/starter/node_modules/@donkeylabs/server/mcp/server.ts +3238 -0
- package/examples/starter/node_modules/@donkeylabs/server/package.json +77 -0
- package/examples/starter/node_modules/@donkeylabs/server/registry.d.ts +11 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/client/base.ts +481 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/client/index.ts +150 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/cache.ts +183 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/cron.ts +255 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/errors.ts +320 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/events.ts +163 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/index.ts +94 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/jobs.ts +334 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/logger.ts +131 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/rate-limiter.ts +193 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/sse.ts +210 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core.ts +428 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/handlers.ts +87 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/harness.ts +70 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/index.ts +38 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/middleware.ts +34 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/registry.ts +13 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/router.ts +155 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/server.ts +234 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/init/donkeylabs.config.ts.template +14 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/init/index.ts.template +41 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/plugin/index.ts.template +25 -0
- package/examples/starter/src/routes/health/ping/models/model.ts +11 -7
- package/package.json +3 -3
- package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-native-jsx.d.ts +0 -32
- package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-shims-v4.d.ts +0 -290
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# API Client
|
|
2
|
+
|
|
3
|
+
Code-generated, fully-typed API client for consuming routes with TypeScript. Supports typed requests, SSE events, and automatic authentication handling.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// Generate client from your server routes
|
|
9
|
+
// bun run gen:client server.ts
|
|
10
|
+
|
|
11
|
+
// Import and use
|
|
12
|
+
import { createApiClient } from "./client";
|
|
13
|
+
|
|
14
|
+
const api = createApiClient({ baseUrl: "http://localhost:3000" });
|
|
15
|
+
|
|
16
|
+
// Typed route calls
|
|
17
|
+
const user = await api.users.get({ id: 1 });
|
|
18
|
+
console.log(user.name); // Fully typed!
|
|
19
|
+
|
|
20
|
+
// SSE events with typed handlers
|
|
21
|
+
api.connect();
|
|
22
|
+
api.on("notifications.new", (data) => {
|
|
23
|
+
console.log(data.message); // Typed!
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Generation
|
|
30
|
+
|
|
31
|
+
### CLI Command
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
# Generate from specific server files
|
|
35
|
+
bun scripts/generate-client.ts server.ts
|
|
36
|
+
|
|
37
|
+
# Generate from multiple files
|
|
38
|
+
bun scripts/generate-client.ts server.ts api.ts
|
|
39
|
+
|
|
40
|
+
# Custom output location
|
|
41
|
+
bun scripts/generate-client.ts --output ./src/api server.ts
|
|
42
|
+
|
|
43
|
+
# Using npm script
|
|
44
|
+
bun run gen:client server.ts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Options
|
|
48
|
+
|
|
49
|
+
| Option | Default | Description |
|
|
50
|
+
|--------|---------|-------------|
|
|
51
|
+
| `--output <path>` | `./client` | Output directory for generated files |
|
|
52
|
+
| `--name <name>` | `index.ts` | Generated client filename |
|
|
53
|
+
| `--help` | - | Show usage information |
|
|
54
|
+
|
|
55
|
+
### Generated Files
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
client/
|
|
59
|
+
├── base.ts # Runtime (ApiClientBase, errors, types)
|
|
60
|
+
└── index.ts # Generated client with typed routes/events
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Client Configuration
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
interface ApiClientConfig {
|
|
69
|
+
/** Base URL of the API server */
|
|
70
|
+
baseUrl: string;
|
|
71
|
+
|
|
72
|
+
/** Default headers for all requests */
|
|
73
|
+
headers?: Record<string, string>;
|
|
74
|
+
|
|
75
|
+
/** Credentials mode (default: "include" for cookies) */
|
|
76
|
+
credentials?: "include" | "same-origin" | "omit";
|
|
77
|
+
|
|
78
|
+
/** Custom fetch implementation (for Node.js or testing) */
|
|
79
|
+
fetch?: typeof fetch;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const api = createApiClient({
|
|
83
|
+
baseUrl: "http://localhost:3000",
|
|
84
|
+
headers: {
|
|
85
|
+
"X-Client-Version": "1.0.0",
|
|
86
|
+
},
|
|
87
|
+
credentials: "include",
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Route Calls
|
|
94
|
+
|
|
95
|
+
### Typed Routes
|
|
96
|
+
|
|
97
|
+
Generated from `.route().typed()` definitions:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// Server-side definition
|
|
101
|
+
router.route("users.get").typed({
|
|
102
|
+
input: z.object({ id: z.number() }),
|
|
103
|
+
output: z.object({ id: z.number(), name: z.string(), email: z.string() }),
|
|
104
|
+
handle: async (input, ctx) => { ... },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Generated client method
|
|
108
|
+
const user = await api.users.get({ id: 1 });
|
|
109
|
+
// user: { id: number; name: string; email: string }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Route Namespaces
|
|
113
|
+
|
|
114
|
+
Routes are grouped by prefix:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// Server: createRouter("users")
|
|
118
|
+
api.users.get({ id: 1 });
|
|
119
|
+
api.users.create({ name: "Alice" });
|
|
120
|
+
api.users.update({ id: 1, name: "Bob" });
|
|
121
|
+
|
|
122
|
+
// Server: createRouter("orders")
|
|
123
|
+
api.orders.list({ page: 1 });
|
|
124
|
+
api.orders.create({ items: [...] });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Request Options
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// Abort signal
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
const user = await api.users.get({ id: 1 }, {
|
|
133
|
+
signal: controller.signal,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Custom headers per request
|
|
137
|
+
const order = await api.orders.create({ items: [...] }, {
|
|
138
|
+
headers: { "X-Idempotency-Key": "unique-key" },
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Raw Routes
|
|
143
|
+
|
|
144
|
+
For non-JSON endpoints:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// Server-side
|
|
148
|
+
router.route("download").raw({
|
|
149
|
+
handle: async (req, ctx) => new Response(fileBuffer),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Client-side (returns raw Response)
|
|
153
|
+
const response = await api.files.download();
|
|
154
|
+
const blob = await response.blob();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Error Handling
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { ApiError, ValidationError } from "./client";
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const user = await api.users.create({ email: "invalid" });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof ValidationError) {
|
|
168
|
+
// Zod validation failed (400)
|
|
169
|
+
console.log("Validation errors:", error.details);
|
|
170
|
+
// [{ path: ["email"], message: "Invalid email" }]
|
|
171
|
+
} else if (error instanceof ApiError) {
|
|
172
|
+
// HTTP error
|
|
173
|
+
console.log("Status:", error.status);
|
|
174
|
+
console.log("Body:", error.body);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Error Types
|
|
180
|
+
|
|
181
|
+
| Error | Status | Description |
|
|
182
|
+
|-------|--------|-------------|
|
|
183
|
+
| `ValidationError` | 400 | Zod schema validation failed |
|
|
184
|
+
| `ApiError` | Any | Generic HTTP error with status/body |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## SSE Events
|
|
189
|
+
|
|
190
|
+
### Connection
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// Connect to SSE endpoint
|
|
194
|
+
api.connect();
|
|
195
|
+
// Or with options
|
|
196
|
+
api.connect({
|
|
197
|
+
endpoint: "/events", // Default: "/sse"
|
|
198
|
+
channels: ["orders"], // Subscribe to specific channels
|
|
199
|
+
autoReconnect: true, // Auto-reconnect on disconnect (default)
|
|
200
|
+
reconnectDelay: 3000, // Reconnect delay in ms (default: 3000)
|
|
201
|
+
onConnect: () => console.log("Connected"),
|
|
202
|
+
onDisconnect: () => console.log("Disconnected"),
|
|
203
|
+
onError: (e) => console.error("SSE error", e),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Check connection status
|
|
207
|
+
console.log(api.connected); // boolean
|
|
208
|
+
|
|
209
|
+
// Disconnect
|
|
210
|
+
api.disconnect();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Typed Event Handlers
|
|
214
|
+
|
|
215
|
+
Events are typed from plugin `events` definitions:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// Plugin definition
|
|
219
|
+
export const notificationsPlugin = createPlugin.define({
|
|
220
|
+
name: "notifications",
|
|
221
|
+
events: {
|
|
222
|
+
new: z.object({
|
|
223
|
+
id: z.number(),
|
|
224
|
+
message: z.string(),
|
|
225
|
+
type: z.enum(["info", "warning", "error"]),
|
|
226
|
+
}),
|
|
227
|
+
unreadCount: z.object({ count: z.number() }),
|
|
228
|
+
},
|
|
229
|
+
// ...
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Client-side (generated types)
|
|
233
|
+
api.on("notifications.new", (data) => {
|
|
234
|
+
// data: { id: number; message: string; type: "info" | "warning" | "error" }
|
|
235
|
+
showToast(data.message, data.type);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
api.on("notifications.unreadCount", (data) => {
|
|
239
|
+
// data: { count: number }
|
|
240
|
+
updateBadge(data.count);
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Event Subscription
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// Subscribe to event (returns unsubscribe function)
|
|
248
|
+
const unsubscribe = api.on("orders.statusChanged", (data) => {
|
|
249
|
+
console.log("Order status:", data.status);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Later: unsubscribe
|
|
253
|
+
unsubscribe();
|
|
254
|
+
|
|
255
|
+
// One-time subscription
|
|
256
|
+
api.once("orders.statusChanged", (data) => {
|
|
257
|
+
console.log("First status change:", data.status);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Remove all handlers for event
|
|
261
|
+
api.off("orders.statusChanged");
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Authentication
|
|
267
|
+
|
|
268
|
+
### HTTP-Only Cookies (Recommended)
|
|
269
|
+
|
|
270
|
+
The client uses `credentials: "include"` by default, which automatically sends and receives HTTP-only cookies:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
// Login sets HTTP-only cookie automatically
|
|
274
|
+
await api.auth.login({ username: "alice", password: "secret" });
|
|
275
|
+
|
|
276
|
+
// Subsequent requests include cookie automatically
|
|
277
|
+
const user = await api.users.me(); // Authenticated!
|
|
278
|
+
|
|
279
|
+
// Logout clears cookie
|
|
280
|
+
await api.auth.logout();
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Custom Auth Headers
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const api = createApiClient({
|
|
287
|
+
baseUrl: "http://localhost:3000",
|
|
288
|
+
headers: {
|
|
289
|
+
Authorization: `Bearer ${token}`,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Or update per-request
|
|
294
|
+
await api.users.get({ id: 1 }, {
|
|
295
|
+
headers: { Authorization: `Bearer ${newToken}` },
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Plugin Client Configuration
|
|
302
|
+
|
|
303
|
+
Plugins can configure client behavior:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// plugins/auth/index.ts
|
|
307
|
+
export const authPlugin = createPlugin.define({
|
|
308
|
+
name: "auth",
|
|
309
|
+
client: {
|
|
310
|
+
credentials: "include", // Ensures cookies are sent
|
|
311
|
+
},
|
|
312
|
+
// ...
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The generator merges all plugin client configs to determine defaults.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Browser & Node.js
|
|
321
|
+
|
|
322
|
+
### Browser
|
|
323
|
+
|
|
324
|
+
Works out of the box with native `fetch` and `EventSource`:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { createApiClient } from "./client";
|
|
328
|
+
const api = createApiClient({ baseUrl: "http://localhost:3000" });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Node.js / Bun
|
|
332
|
+
|
|
333
|
+
Native fetch is available in Bun and Node.js 18+:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { createApiClient } from "./client";
|
|
337
|
+
|
|
338
|
+
const api = createApiClient({
|
|
339
|
+
baseUrl: "http://localhost:3000",
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
For SSE in Node.js, use `eventsource` polyfill:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import EventSource from "eventsource";
|
|
347
|
+
globalThis.EventSource = EventSource;
|
|
348
|
+
|
|
349
|
+
// Then use normally
|
|
350
|
+
api.connect();
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Complete Example
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { createApiClient, ApiError, ValidationError } from "./client";
|
|
359
|
+
|
|
360
|
+
// Create client
|
|
361
|
+
const api = createApiClient({ baseUrl: "http://localhost:3000" });
|
|
362
|
+
|
|
363
|
+
async function main() {
|
|
364
|
+
try {
|
|
365
|
+
// Login
|
|
366
|
+
await api.auth.login({ username: "alice", password: "secret" });
|
|
367
|
+
|
|
368
|
+
// Connect to SSE
|
|
369
|
+
api.connect({
|
|
370
|
+
onConnect: () => console.log("SSE connected"),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Listen for events
|
|
374
|
+
api.on("notifications.new", (data) => {
|
|
375
|
+
console.log(`New notification: ${data.message}`);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Make typed API calls
|
|
379
|
+
const user = await api.users.get({ id: 1 });
|
|
380
|
+
console.log(`Hello, ${user.name}!`);
|
|
381
|
+
|
|
382
|
+
// Create order
|
|
383
|
+
const order = await api.orders.create({
|
|
384
|
+
items: [
|
|
385
|
+
{ productId: 1, quantity: 2 },
|
|
386
|
+
{ productId: 3, quantity: 1 },
|
|
387
|
+
],
|
|
388
|
+
});
|
|
389
|
+
console.log(`Order created: ${order.id}`);
|
|
390
|
+
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (error instanceof ValidationError) {
|
|
393
|
+
console.error("Validation failed:", error.details);
|
|
394
|
+
} else if (error instanceof ApiError) {
|
|
395
|
+
console.error(`API error ${error.status}:`, error.body);
|
|
396
|
+
} else {
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Cleanup on exit
|
|
403
|
+
process.on("SIGINT", () => {
|
|
404
|
+
api.disconnect();
|
|
405
|
+
process.exit();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
main();
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Type Generation
|
|
414
|
+
|
|
415
|
+
### How It Works
|
|
416
|
+
|
|
417
|
+
The generator scans your code for:
|
|
418
|
+
|
|
419
|
+
1. **Routes**: Extracts `createRouter()` calls and `.route().typed()` definitions
|
|
420
|
+
2. **Events**: Scans `plugins/*/index.ts` for `events: { ... }` definitions
|
|
421
|
+
3. **Client Config**: Reads `client: { ... }` from plugins
|
|
422
|
+
|
|
423
|
+
Zod schemas are converted to TypeScript types:
|
|
424
|
+
|
|
425
|
+
| Zod Schema | Generated Type |
|
|
426
|
+
|------------|----------------|
|
|
427
|
+
| `z.string()` | `string` |
|
|
428
|
+
| `z.number()` | `number` |
|
|
429
|
+
| `z.boolean()` | `boolean` |
|
|
430
|
+
| `z.object({ a: z.string() })` | `{ a: string }` |
|
|
431
|
+
| `z.array(z.number())` | `number[]` |
|
|
432
|
+
| `z.enum(["a", "b"])` | `"a" \| "b"` |
|
|
433
|
+
| `z.optional()` | `T \| undefined` |
|
|
434
|
+
| `z.nullable()` | `T \| null` |
|
|
435
|
+
|
|
436
|
+
### Regenerating
|
|
437
|
+
|
|
438
|
+
Run the generator when you:
|
|
439
|
+
|
|
440
|
+
- Add new routes
|
|
441
|
+
- Modify route input/output schemas
|
|
442
|
+
- Add plugin events
|
|
443
|
+
- Change plugin client config
|
|
444
|
+
|
|
445
|
+
```sh
|
|
446
|
+
bun run gen:client server.ts
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Best Practices
|
|
452
|
+
|
|
453
|
+
### 1. Regenerate After Schema Changes
|
|
454
|
+
|
|
455
|
+
```sh
|
|
456
|
+
# After modifying routes or events
|
|
457
|
+
bun run gen:client server.ts
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 2. Handle Errors Gracefully
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
async function fetchUser(id: number) {
|
|
464
|
+
try {
|
|
465
|
+
return await api.users.get({ id });
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 3. Use Abort Controllers for Cancellation
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const controller = new AbortController();
|
|
479
|
+
|
|
480
|
+
// Cancel after 5 seconds
|
|
481
|
+
setTimeout(() => controller.abort(), 5000);
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
await api.reports.generate({ type: "annual" }, {
|
|
485
|
+
signal: controller.signal,
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (error.name === "AbortError") {
|
|
489
|
+
console.log("Request cancelled");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### 4. Reconnect SSE on Auth Changes
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
// After login, reconnect to get authenticated events
|
|
498
|
+
await api.auth.login({ ... });
|
|
499
|
+
api.disconnect();
|
|
500
|
+
api.connect();
|
|
501
|
+
|
|
502
|
+
// After logout
|
|
503
|
+
await api.auth.logout();
|
|
504
|
+
api.disconnect();
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### 5. Clean Up SSE on Unmount
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
// React example
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
api.connect();
|
|
513
|
+
const unsub = api.on("updates", setData);
|
|
514
|
+
|
|
515
|
+
return () => {
|
|
516
|
+
unsub();
|
|
517
|
+
api.disconnect();
|
|
518
|
+
};
|
|
519
|
+
}, []);
|
|
520
|
+
```
|