@donkeylabs/server 0.3.0 → 0.4.0

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.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/docs/api-client.md +7 -7
  3. package/docs/cache.md +1 -74
  4. package/docs/core-services.md +4 -116
  5. package/docs/cron.md +1 -1
  6. package/docs/errors.md +2 -2
  7. package/docs/events.md +3 -98
  8. package/docs/handlers.md +13 -48
  9. package/docs/logger.md +3 -58
  10. package/docs/middleware.md +2 -2
  11. package/docs/plugins.md +13 -64
  12. package/docs/project-structure.md +4 -142
  13. package/docs/rate-limiter.md +4 -136
  14. package/docs/router.md +6 -14
  15. package/docs/sse.md +1 -99
  16. package/docs/sveltekit-adapter.md +420 -0
  17. package/package.json +8 -11
  18. package/registry.d.ts +15 -14
  19. package/src/core/cache.ts +0 -75
  20. package/src/core/cron.ts +3 -96
  21. package/src/core/errors.ts +78 -11
  22. package/src/core/events.ts +1 -47
  23. package/src/core/index.ts +0 -4
  24. package/src/core/jobs.ts +0 -112
  25. package/src/core/logger.ts +12 -79
  26. package/src/core/rate-limiter.ts +29 -108
  27. package/src/core/sse.ts +1 -84
  28. package/src/core.ts +13 -104
  29. package/src/generator/index.ts +566 -0
  30. package/src/generator/zod-to-ts.ts +114 -0
  31. package/src/handlers.ts +14 -110
  32. package/src/index.ts +30 -24
  33. package/src/middleware.ts +2 -5
  34. package/src/registry.ts +4 -0
  35. package/src/router.ts +47 -1
  36. package/src/server.ts +618 -332
  37. package/README.md +0 -254
  38. package/cli/commands/dev.ts +0 -134
  39. package/cli/commands/generate.ts +0 -605
  40. package/cli/commands/init.ts +0 -205
  41. package/cli/commands/interactive.ts +0 -417
  42. package/cli/commands/plugin.ts +0 -192
  43. package/cli/commands/route.ts +0 -195
  44. package/cli/donkeylabs +0 -2
  45. package/cli/index.ts +0 -114
  46. package/docs/svelte-frontend.md +0 -324
  47. package/docs/testing.md +0 -438
  48. package/mcp/donkeylabs-mcp +0 -3238
  49. 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 (validates data) |
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,14 +1,10 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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
- "bin": {
9
- "donkeylabs": "./cli/index.ts",
10
- "donkeylabs-mcp": "./mcp/server.ts"
11
- },
12
8
  "exports": {
13
9
  ".": {
14
10
  "types": "./src/index.ts",
@@ -22,15 +18,17 @@
22
18
  "types": "./src/harness.ts",
23
19
  "import": "./src/harness.ts"
24
20
  },
21
+ "./generator": {
22
+ "types": "./src/generator/index.ts",
23
+ "import": "./src/generator/index.ts"
24
+ },
25
25
  "./context": {
26
26
  "types": "./context.d.ts"
27
- }
27
+ },
28
+ "./docs/*": "./docs/*"
28
29
  },
29
- "workspaces": ["test-todo-app"],
30
30
  "files": [
31
31
  "src",
32
- "cli",
33
- "mcp",
34
32
  "docs",
35
33
  "context.d.ts",
36
34
  "registry.d.ts",
@@ -41,7 +39,6 @@
41
39
  "gen:registry": "bun scripts/generate-registry.ts",
42
40
  "gen:server": "bun scripts/generate-server.ts",
43
41
  "gen:client": "bun scripts/generate-client.ts",
44
- "cli": "bun cli/index.ts",
45
42
  "test": "bun test",
46
43
  "typecheck": "bun --bun tsc --noEmit",
47
44
  "dev": "bun --watch src/index.ts",
@@ -79,4 +76,4 @@
79
76
  "url": "https://github.com/donkeylabs/server"
80
77
  },
81
78
  "license": "MIT"
82
- }
79
+ }
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
- stats: Register<InferService<typeof statsPlugin>, InferSchema<typeof statsPlugin>, InferHandlers<typeof statsPlugin>, InferDependencies<typeof statsPlugin>, InferMiddleware<typeof statsPlugin>>;
10
+ // Plugin types are added here when running `donkeylabs generate`
8
11
  }
9
12
 
10
- export interface PluginHandlerRegistry extends
11
- // Merge all plugin handlers
12
- InferHandlers<typeof statsPlugin>
13
- {}
13
+ export interface PluginHandlerRegistry {
14
+ // Custom handler types are merged here
15
+ }
14
16
 
15
- export interface PluginMiddlewareRegistry extends
16
- // Merge all plugin middleware
17
- InferMiddleware<typeof statsPlugin>
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
- // Augment IRouteBuilder interface with custom handler methods
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
  }