@donkeylabs/server 0.3.0 → 0.3.1
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/LICENSE +1 -1
- package/docs/api-client.md +7 -7
- package/docs/cache.md +1 -74
- package/docs/core-services.md +4 -116
- package/docs/cron.md +1 -1
- package/docs/errors.md +2 -2
- package/docs/events.md +3 -98
- package/docs/handlers.md +13 -48
- package/docs/logger.md +3 -58
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +13 -64
- package/docs/project-structure.md +4 -142
- package/docs/rate-limiter.md +4 -136
- package/docs/router.md +6 -14
- package/docs/sse.md +1 -99
- package/docs/sveltekit-adapter.md +420 -0
- package/package.json +6 -6
- package/registry.d.ts +15 -14
- package/src/core/cache.ts +0 -75
- package/src/core/cron.ts +3 -96
- package/src/core/errors.ts +78 -11
- package/src/core/events.ts +1 -47
- package/src/core/index.ts +0 -4
- package/src/core/jobs.ts +0 -112
- package/src/core/logger.ts +12 -79
- package/src/core/rate-limiter.ts +29 -108
- package/src/core/sse.ts +1 -84
- package/src/core.ts +13 -104
- package/src/generator/index.ts +551 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +19 -23
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/server.ts +354 -337
- package/README.md +0 -254
- package/cli/commands/dev.ts +0 -134
- package/cli/commands/generate.ts +0 -605
- package/cli/commands/init.ts +0 -205
- package/cli/commands/interactive.ts +0 -417
- package/cli/commands/plugin.ts +0 -192
- package/cli/commands/route.ts +0 -195
- package/cli/donkeylabs +0 -2
- package/cli/index.ts +0 -114
- package/docs/svelte-frontend.md +0 -324
- package/docs/testing.md +0 -438
- package/mcp/donkeylabs-mcp +0 -3238
- package/mcp/server.ts +0 -3238
package/docs/sse.md
CHANGED
|
@@ -28,14 +28,6 @@ ctx.core.sse.broadcast(`user:${userId}`, "notification", {
|
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
30
|
interface SSE {
|
|
31
|
-
// Channel registration (required before broadcasting)
|
|
32
|
-
registerChannel(name: string, config?: SSEChannelConfig): void;
|
|
33
|
-
registerChannels(channels: Record<string, SSEChannelConfig>): void;
|
|
34
|
-
isChannelRegistered(channel: string): boolean;
|
|
35
|
-
getChannelConfig(channel: string): SSEChannelConfig | undefined;
|
|
36
|
-
listChannels(): string[];
|
|
37
|
-
|
|
38
|
-
// Client management
|
|
39
31
|
addClient(options?: { lastEventId?: string }): { client: SSEClient; response: Response };
|
|
40
32
|
removeClient(clientId: string): void;
|
|
41
33
|
getClient(clientId: string): SSEClient | undefined;
|
|
@@ -49,12 +41,6 @@ interface SSE {
|
|
|
49
41
|
shutdown(): void;
|
|
50
42
|
}
|
|
51
43
|
|
|
52
|
-
interface SSEChannelConfig {
|
|
53
|
-
description?: string;
|
|
54
|
-
pattern?: boolean; // For dynamic channels like "user:*"
|
|
55
|
-
events?: Record<string, z.ZodType<any>>; // Event schemas for validation
|
|
56
|
-
}
|
|
57
|
-
|
|
58
44
|
interface SSEClient {
|
|
59
45
|
id: string;
|
|
60
46
|
channels: Set<string>;
|
|
@@ -68,16 +54,12 @@ interface SSEClient {
|
|
|
68
54
|
|
|
69
55
|
| Method | Description |
|
|
70
56
|
|--------|-------------|
|
|
71
|
-
| `registerChannel(name, config?)` | Register channel (required before broadcast) |
|
|
72
|
-
| `registerChannels(channels)` | Register multiple channels at once |
|
|
73
|
-
| `isChannelRegistered(channel)` | Check if channel is registered |
|
|
74
|
-
| `listChannels()` | List all registered channel names |
|
|
75
57
|
| `addClient(opts?)` | Create new SSE client, returns client and response |
|
|
76
58
|
| `removeClient(id)` | Disconnect and remove client |
|
|
77
59
|
| `getClient(id)` | Get client by ID |
|
|
78
60
|
| `subscribe(clientId, channel)` | Subscribe client to channel |
|
|
79
61
|
| `unsubscribe(clientId, channel)` | Unsubscribe from channel |
|
|
80
|
-
| `broadcast(channel, event, data, id?)` | Send to all channel subscribers
|
|
62
|
+
| `broadcast(channel, event, data, id?)` | Send to all channel subscribers |
|
|
81
63
|
| `broadcastAll(event, data, id?)` | Send to all connected clients |
|
|
82
64
|
| `sendTo(clientId, event, data, id?)` | Send to specific client |
|
|
83
65
|
| `getClients()` | Get all connected clients |
|
|
@@ -100,86 +82,6 @@ const server = new AppServer({
|
|
|
100
82
|
|
|
101
83
|
---
|
|
102
84
|
|
|
103
|
-
## Channel Registration
|
|
104
|
-
|
|
105
|
-
**Channels must be registered before broadcasting.** This ensures type safety and validates event data.
|
|
106
|
-
|
|
107
|
-
### Server-Level Registration
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
// Register simple channels
|
|
111
|
-
server.registerSSEChannel("notifications");
|
|
112
|
-
server.registerSSEChannel("dashboard");
|
|
113
|
-
|
|
114
|
-
// Register with event schemas for validation
|
|
115
|
-
server.registerSSEChannel("orders", {
|
|
116
|
-
events: {
|
|
117
|
-
newOrder: z.object({ id: z.string(), total: z.number() }),
|
|
118
|
-
statusUpdate: z.object({ id: z.string(), status: z.string() }),
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Pattern Channels
|
|
124
|
-
|
|
125
|
-
For dynamic channels like user-specific streams:
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
// Register pattern channel (matches user:123, user:456, etc.)
|
|
129
|
-
server.registerSSEChannel("user:*", { pattern: true });
|
|
130
|
-
|
|
131
|
-
// Now these all work:
|
|
132
|
-
ctx.core.sse.broadcast("user:123", "notification", data);
|
|
133
|
-
ctx.core.sse.broadcast("user:456", "message", data);
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Plugin Channels
|
|
137
|
-
|
|
138
|
-
Plugins can declare their SSE channels:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
export const notificationsPlugin = createPlugin.define({
|
|
142
|
-
name: "notifications",
|
|
143
|
-
sseChannels: {
|
|
144
|
-
"notifications": {
|
|
145
|
-
events: {
|
|
146
|
-
new: z.object({ id: z.string(), message: z.string() }),
|
|
147
|
-
read: z.object({ id: z.string() }),
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
"user:*": { pattern: true },
|
|
151
|
-
},
|
|
152
|
-
service: async (ctx) => ({ /* ... */ }),
|
|
153
|
-
});
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Validation
|
|
157
|
-
|
|
158
|
-
Broadcasting to unregistered channels or with invalid data throws:
|
|
159
|
-
|
|
160
|
-
```ts
|
|
161
|
-
// Throws: SSE channel 'unknown' is not registered
|
|
162
|
-
ctx.core.sse.broadcast("unknown", "event", data);
|
|
163
|
-
|
|
164
|
-
// Throws: SSE event 'newOrder' validation failed
|
|
165
|
-
ctx.core.sse.broadcast("orders", "newOrder", { id: 123 }); // id should be string
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Introspection
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
// Check if channel is registered
|
|
172
|
-
if (ctx.core.sse.isChannelRegistered("notifications")) {
|
|
173
|
-
ctx.core.sse.broadcast("notifications", "alert", data);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// List all registered channels
|
|
177
|
-
const channels = ctx.core.sse.listChannels();
|
|
178
|
-
// ["notifications", "dashboard", "orders", "user:*"]
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
85
|
## Usage Examples
|
|
184
86
|
|
|
185
87
|
### Basic SSE Endpoint
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# SvelteKit Adapter
|
|
2
|
+
|
|
3
|
+
`@donkeylabs/adapter-sveltekit` integrates @donkeylabs/server with SvelteKit, running both in a single Bun process.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Single Process** - One Bun.serve() handles SvelteKit pages and API routes
|
|
8
|
+
- **SSR Direct Calls** - No HTTP overhead during server-side rendering
|
|
9
|
+
- **Unified API Client** - Same interface in SSR and browser
|
|
10
|
+
- **SSE Support** - Real-time server-sent events in the browser
|
|
11
|
+
- **Type Safety** - Full TypeScript support throughout
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @donkeylabs/adapter-sveltekit @donkeylabs/server
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick Setup
|
|
24
|
+
|
|
25
|
+
### 1. Configure the Adapter
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
// svelte.config.js
|
|
29
|
+
import adapter from '@donkeylabs/adapter-sveltekit';
|
|
30
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
preprocess: vitePreprocess(),
|
|
34
|
+
kit: {
|
|
35
|
+
adapter: adapter({
|
|
36
|
+
serverEntry: './src/server/index.ts',
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Create the Server Entry
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// src/server/index.ts
|
|
46
|
+
import { AppServer, createPlugin, createRouter } from "@donkeylabs/server";
|
|
47
|
+
import { Kysely } from "kysely";
|
|
48
|
+
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
49
|
+
import { Database } from "bun:sqlite";
|
|
50
|
+
import { z } from "zod";
|
|
51
|
+
|
|
52
|
+
// Database setup
|
|
53
|
+
const db = new Kysely<{}>({
|
|
54
|
+
dialect: new BunSqliteDialect({ database: new Database(":memory:") }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Create a plugin
|
|
58
|
+
const myPlugin = createPlugin.define({
|
|
59
|
+
name: "myPlugin",
|
|
60
|
+
service: async (ctx) => ({
|
|
61
|
+
getData: () => ({ message: "Hello from plugin!" }),
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Create routes
|
|
66
|
+
const api = createRouter("api");
|
|
67
|
+
|
|
68
|
+
api.route("data.get").typed({
|
|
69
|
+
handle: async (_input, ctx) => ctx.plugins.myPlugin.getData(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Create and export the server
|
|
73
|
+
export const server = new AppServer({
|
|
74
|
+
db,
|
|
75
|
+
port: 0, // Port managed by adapter
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
server.registerPlugin(myPlugin);
|
|
79
|
+
server.use(api);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Set Up Hooks
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// src/hooks.server.ts
|
|
86
|
+
import { createHandle } from "@donkeylabs/adapter-sveltekit/hooks";
|
|
87
|
+
|
|
88
|
+
export const handle = createHandle();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 4. Create the API Client
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// src/lib/api.ts
|
|
95
|
+
import { UnifiedApiClientBase } from "@donkeylabs/adapter-sveltekit/client";
|
|
96
|
+
|
|
97
|
+
interface DataResponse {
|
|
98
|
+
message: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class ApiClient extends UnifiedApiClientBase {
|
|
102
|
+
data = {
|
|
103
|
+
get: () => this.request<{}, DataResponse>("api.data.get", {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createApi(options?: { locals?: any }) {
|
|
108
|
+
return new ApiClient(options);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
### SSR (Server-Side Rendering)
|
|
117
|
+
|
|
118
|
+
In `+page.server.ts`, pass `locals` to get direct service calls without HTTP:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// src/routes/+page.server.ts
|
|
122
|
+
import type { PageServerLoad } from './$types';
|
|
123
|
+
import { createApi } from '$lib/api';
|
|
124
|
+
|
|
125
|
+
export const load: PageServerLoad = async ({ locals }) => {
|
|
126
|
+
// Pass locals for direct calls (no HTTP!)
|
|
127
|
+
const api = createApi({ locals });
|
|
128
|
+
|
|
129
|
+
const data = await api.data.get();
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
message: data.message,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Browser
|
|
138
|
+
|
|
139
|
+
In `+page.svelte`, create the client without locals:
|
|
140
|
+
|
|
141
|
+
```svelte
|
|
142
|
+
<script lang="ts">
|
|
143
|
+
import { createApi } from '$lib/api';
|
|
144
|
+
|
|
145
|
+
let { data } = $props();
|
|
146
|
+
|
|
147
|
+
// Browser client - uses HTTP calls
|
|
148
|
+
const api = createApi();
|
|
149
|
+
|
|
150
|
+
async function refresh() {
|
|
151
|
+
const result = await api.data.get();
|
|
152
|
+
data.message = result.message;
|
|
153
|
+
}
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<h1>{data.message}</h1>
|
|
157
|
+
<button onclick={refresh}>Refresh</button>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## SSE (Server-Sent Events)
|
|
163
|
+
|
|
164
|
+
The unified client includes SSE support for real-time updates.
|
|
165
|
+
|
|
166
|
+
### Server Setup
|
|
167
|
+
|
|
168
|
+
Broadcast events from your plugin:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// In plugin init or service method
|
|
172
|
+
ctx.core.sse.broadcast("notifications", "new-message", {
|
|
173
|
+
id: Date.now(),
|
|
174
|
+
text: "Hello!",
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Client Subscription
|
|
179
|
+
|
|
180
|
+
```svelte
|
|
181
|
+
<script lang="ts">
|
|
182
|
+
import { onMount } from 'svelte';
|
|
183
|
+
import { browser } from '$app/environment';
|
|
184
|
+
import { createApi } from '$lib/api';
|
|
185
|
+
|
|
186
|
+
const api = createApi();
|
|
187
|
+
let messages = $state<Array<{ id: number; text: string }>>([]);
|
|
188
|
+
|
|
189
|
+
onMount(() => {
|
|
190
|
+
if (!browser) return;
|
|
191
|
+
|
|
192
|
+
// Subscribe to SSE channel
|
|
193
|
+
const unsubscribe = api.sse.subscribe(
|
|
194
|
+
['notifications'],
|
|
195
|
+
(eventType, eventData) => {
|
|
196
|
+
if (eventType === 'new-message') {
|
|
197
|
+
messages = [eventData, ...messages];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return unsubscribe;
|
|
203
|
+
});
|
|
204
|
+
</script>
|
|
205
|
+
|
|
206
|
+
<ul>
|
|
207
|
+
{#each messages as msg}
|
|
208
|
+
<li>{msg.text}</li>
|
|
209
|
+
{/each}
|
|
210
|
+
</ul>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## How It Works
|
|
216
|
+
|
|
217
|
+
### Request Flow
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
Request → Bun.serve()
|
|
221
|
+
│
|
|
222
|
+
├─ POST /api.route.name → AppServer.handleRequest() → Response
|
|
223
|
+
│
|
|
224
|
+
├─ GET /sse?channels=... → SSE stream
|
|
225
|
+
│
|
|
226
|
+
└─ GET /page → SvelteKit.respond()
|
|
227
|
+
│
|
|
228
|
+
└─ hooks.server.ts handle()
|
|
229
|
+
│
|
|
230
|
+
└─ locals.handleRoute = direct caller
|
|
231
|
+
│
|
|
232
|
+
└─ +page.server.ts load()
|
|
233
|
+
│
|
|
234
|
+
└─ api.route() → DIRECT (no HTTP)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### SSR vs Browser
|
|
238
|
+
|
|
239
|
+
| Environment | API Client | Transport |
|
|
240
|
+
|-------------|------------|-----------|
|
|
241
|
+
| `+page.server.ts` | `createApi({ locals })` | Direct function call |
|
|
242
|
+
| `+page.svelte` | `createApi()` | HTTP POST |
|
|
243
|
+
| SSE subscription | `api.sse.subscribe()` | EventSource (browser only) |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Adapter Options
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
adapter({
|
|
251
|
+
// Required: Path to your @donkeylabs/server setup
|
|
252
|
+
serverEntry: './src/server/index.ts',
|
|
253
|
+
|
|
254
|
+
// Optional: Output directory (default: "build")
|
|
255
|
+
out: 'build',
|
|
256
|
+
|
|
257
|
+
// Optional: Precompress static assets (default: true)
|
|
258
|
+
precompress: true,
|
|
259
|
+
|
|
260
|
+
// Optional: Environment variable prefix (default: "")
|
|
261
|
+
envPrefix: '',
|
|
262
|
+
})
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Building and Running
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Development
|
|
271
|
+
bun run dev
|
|
272
|
+
|
|
273
|
+
# Build
|
|
274
|
+
bun run build
|
|
275
|
+
|
|
276
|
+
# Production
|
|
277
|
+
PORT=3000 bun build/server/entry.js
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Accessing Server Context
|
|
283
|
+
|
|
284
|
+
The hooks provide access to server internals through `locals`:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
// In +page.server.ts
|
|
288
|
+
export const load: PageServerLoad = async ({ locals }) => {
|
|
289
|
+
// Direct route handler (for API client)
|
|
290
|
+
locals.handleRoute("api.route.name", input);
|
|
291
|
+
|
|
292
|
+
// Plugin services (direct access)
|
|
293
|
+
locals.plugins.myPlugin.getData();
|
|
294
|
+
|
|
295
|
+
// Core services
|
|
296
|
+
locals.core.logger.info("Hello");
|
|
297
|
+
locals.core.cache.get("key");
|
|
298
|
+
|
|
299
|
+
// Database
|
|
300
|
+
locals.db.selectFrom("users").execute();
|
|
301
|
+
|
|
302
|
+
// Client IP
|
|
303
|
+
locals.ip;
|
|
304
|
+
};
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## TypeScript Setup
|
|
310
|
+
|
|
311
|
+
Add path aliases in `tsconfig.json`:
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
{
|
|
315
|
+
"compilerOptions": {
|
|
316
|
+
"paths": {
|
|
317
|
+
"$lib/*": ["./src/lib/*"]
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Common Patterns
|
|
326
|
+
|
|
327
|
+
### Typed API Client
|
|
328
|
+
|
|
329
|
+
Create a fully typed client that mirrors your routes:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
// src/lib/api.ts
|
|
333
|
+
import { UnifiedApiClientBase } from "@donkeylabs/adapter-sveltekit/client";
|
|
334
|
+
|
|
335
|
+
// Define response types
|
|
336
|
+
interface User { id: string; name: string; }
|
|
337
|
+
interface UsersResponse { users: User[]; }
|
|
338
|
+
|
|
339
|
+
export class ApiClient extends UnifiedApiClientBase {
|
|
340
|
+
users = {
|
|
341
|
+
list: () =>
|
|
342
|
+
this.request<{}, UsersResponse>("api.users.list", {}),
|
|
343
|
+
get: (input: { id: string }) =>
|
|
344
|
+
this.request<typeof input, User>("api.users.get", input),
|
|
345
|
+
create: (input: { name: string }) =>
|
|
346
|
+
this.request<typeof input, User>("api.users.create", input),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function createApi(options?: { locals?: any }) {
|
|
351
|
+
return new ApiClient(options);
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Error Handling
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// +page.server.ts
|
|
359
|
+
export const load: PageServerLoad = async ({ locals }) => {
|
|
360
|
+
const api = createApi({ locals });
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const data = await api.users.get({ id: "123" });
|
|
364
|
+
return { user: data };
|
|
365
|
+
} catch (error) {
|
|
366
|
+
// Handle API errors
|
|
367
|
+
return { user: null, error: "User not found" };
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Real-Time Updates with SSE
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// Server: Broadcast on data changes
|
|
376
|
+
api.route("users.create").typed({
|
|
377
|
+
input: z.object({ name: z.string() }),
|
|
378
|
+
handle: async (input, ctx) => {
|
|
379
|
+
const user = await createUser(input);
|
|
380
|
+
|
|
381
|
+
// Broadcast to connected clients
|
|
382
|
+
ctx.core.sse.broadcast("users", "user-created", user);
|
|
383
|
+
|
|
384
|
+
return user;
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Client: Listen for updates
|
|
389
|
+
api.sse.subscribe(["users"], (event, data) => {
|
|
390
|
+
if (event === "user-created") {
|
|
391
|
+
users = [...users, data];
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Troubleshooting
|
|
399
|
+
|
|
400
|
+
### SSR calls returning errors
|
|
401
|
+
|
|
402
|
+
Ensure you pass `locals` to the API client:
|
|
403
|
+
```ts
|
|
404
|
+
const api = createApi({ locals }); // ✓ Correct
|
|
405
|
+
const api = createApi(); // ✗ Will use HTTP, may fail during SSR
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### SSE events not appearing
|
|
409
|
+
|
|
410
|
+
1. Check the channel name matches between broadcast and subscribe
|
|
411
|
+
2. SSE only works in browser - returns no-op in SSR
|
|
412
|
+
3. Named events require `addEventListener` - the client handles this automatically
|
|
413
|
+
|
|
414
|
+
### Build errors
|
|
415
|
+
|
|
416
|
+
Ensure your `serverEntry` path is correct and exports `server`:
|
|
417
|
+
```ts
|
|
418
|
+
// Must export as named 'server' or default
|
|
419
|
+
export const server = new AppServer({ ... });
|
|
420
|
+
```
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donkeylabs/server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Type-safe plugin system for building RPC-style APIs with Bun",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"donkeylabs": "./cli/index.ts",
|
|
10
9
|
"donkeylabs-mcp": "./mcp/server.ts"
|
|
11
10
|
},
|
|
12
11
|
"exports": {
|
|
@@ -22,14 +21,16 @@
|
|
|
22
21
|
"types": "./src/harness.ts",
|
|
23
22
|
"import": "./src/harness.ts"
|
|
24
23
|
},
|
|
24
|
+
"./generator": {
|
|
25
|
+
"types": "./src/generator/index.ts",
|
|
26
|
+
"import": "./src/generator/index.ts"
|
|
27
|
+
},
|
|
25
28
|
"./context": {
|
|
26
29
|
"types": "./context.d.ts"
|
|
27
30
|
}
|
|
28
31
|
},
|
|
29
|
-
"workspaces": ["test-todo-app"],
|
|
30
32
|
"files": [
|
|
31
33
|
"src",
|
|
32
|
-
"cli",
|
|
33
34
|
"mcp",
|
|
34
35
|
"docs",
|
|
35
36
|
"context.d.ts",
|
|
@@ -41,7 +42,6 @@
|
|
|
41
42
|
"gen:registry": "bun scripts/generate-registry.ts",
|
|
42
43
|
"gen:server": "bun scripts/generate-server.ts",
|
|
43
44
|
"gen:client": "bun scripts/generate-client.ts",
|
|
44
|
-
"cli": "bun cli/index.ts",
|
|
45
45
|
"test": "bun test",
|
|
46
46
|
"typecheck": "bun --bun tsc --noEmit",
|
|
47
47
|
"dev": "bun --watch src/index.ts",
|
|
@@ -79,4 +79,4 @@
|
|
|
79
79
|
"url": "https://github.com/donkeylabs/server"
|
|
80
80
|
},
|
|
81
81
|
"license": "MIT"
|
|
82
|
-
}
|
|
82
|
+
}
|
package/registry.d.ts
CHANGED
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-registry.ts
|
|
2
|
+
// This file provides type augmentation points for plugins.
|
|
3
|
+
// User projects generate their own registry.d.ts via `donkeylabs generate`.
|
|
4
|
+
|
|
2
5
|
import { type Register, type InferService, type InferSchema, type InferHandlers, type InferMiddleware, type InferDependencies } from "./src/core";
|
|
3
|
-
import { statsPlugin } from "./examples/starter/src/plugins/stats";
|
|
4
6
|
|
|
7
|
+
// Plugin Registry - augmented by user projects
|
|
5
8
|
declare module "./src/core" {
|
|
6
9
|
export interface PluginRegistry {
|
|
7
|
-
|
|
10
|
+
// Plugin types are added here when running `donkeylabs generate`
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
export interface PluginHandlerRegistry
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
{}
|
|
13
|
+
export interface PluginHandlerRegistry {
|
|
14
|
+
// Custom handler types are merged here
|
|
15
|
+
}
|
|
14
16
|
|
|
15
|
-
export interface PluginMiddlewareRegistry
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
{}
|
|
17
|
+
export interface PluginMiddlewareRegistry {
|
|
18
|
+
// Custom middleware types are merged here
|
|
19
|
+
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
// Union type for all available handlers
|
|
22
|
+
// Union type for all available handlers (base types)
|
|
22
23
|
export type AvailableHandlers = "typed" | "raw";
|
|
23
24
|
|
|
24
25
|
// Union type for all available middleware
|
|
25
26
|
export type AvailableMiddleware = never;
|
|
26
27
|
|
|
27
|
-
//
|
|
28
|
+
// Route builder augmentation for custom handler methods
|
|
28
29
|
declare module "./src/router" {
|
|
29
30
|
export interface IRouteBuilder<TRouter> {
|
|
30
|
-
|
|
31
|
+
// Custom handler methods are added here
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface IMiddlewareBuilder<TRouter> {
|
|
34
|
-
|
|
35
|
+
// Custom middleware methods are added here
|
|
35
36
|
}
|
|
36
37
|
}
|