@donkeylabs/server 1.1.18 → 1.1.19

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/CLAUDE.md CHANGED
@@ -150,4 +150,8 @@ bun --bun tsc --noEmit # Type check
150
150
  `get_project_info`, `create_plugin`, `add_migration`, `add_service_method`, `create_router`, `add_route`, `generate_types`, `list_plugins`, `scaffold_feature`
151
151
 
152
152
  ## Detailed Docs
153
- See `docs/` for: handlers, middleware, database, plugins, testing, jobs, external-jobs, processes, cron, sse, workflows, router, errors, sveltekit-adapter.
153
+ **For comprehensive documentation, see the `docs/` directory.** Each service has its own detailed guide:
154
+ - Core: logger, cache, events, cron, jobs, external-jobs, processes, workflows, sse, rate-limiter, errors
155
+ - API: router, handlers, middleware
156
+ - Server: lifecycle-hooks, services (custom services)
157
+ - Infrastructure: database, plugins, testing, sveltekit-adapter, api-client
@@ -339,3 +339,10 @@ See individual service documentation for adapter interfaces:
339
339
  - [Events Adapters](events.md#custom-adapters)
340
340
  - [Jobs Adapters](jobs.md#custom-adapters)
341
341
  - [Rate Limiter Adapters](rate-limiter.md#custom-adapters)
342
+
343
+ ---
344
+
345
+ ## Related Documentation
346
+
347
+ - [Custom Services](services.md) - Register app-specific services with `defineService()`
348
+ - [Lifecycle Hooks](lifecycle-hooks.md) - `onReady`, `onShutdown`, `onError` hooks
@@ -0,0 +1,348 @@
1
+ # Server Lifecycle Hooks
2
+
3
+ Lifecycle hooks allow you to execute code at specific points in the server's lifecycle: after initialization, during shutdown, and when errors occur.
4
+
5
+ ## Overview
6
+
7
+ | Hook | When Called | Use Case |
8
+ |------|-------------|----------|
9
+ | `onReady` | After server starts, plugins initialized | Initialize app-specific services, warm caches |
10
+ | `onShutdown` | Before server stops | Cleanup connections, flush buffers |
11
+ | `onError` | On unhandled errors | Error reporting, alerts |
12
+
13
+ ## onReady Hook
14
+
15
+ Called after the server is fully initialized, plugins are ready, and the server is accepting requests.
16
+
17
+ ```typescript
18
+ import { AppServer } from "@donkeylabs/server";
19
+
20
+ const server = new AppServer({ db, port: 3000 });
21
+
22
+ server.onReady(async (ctx) => {
23
+ console.log("Server is ready!");
24
+
25
+ // Access all services
26
+ ctx.core.logger.info("Server started", { port: 3000 });
27
+
28
+ // Initialize app-specific classes
29
+ const dashboard = new AdminDashboard(ctx.plugins.auth);
30
+ await dashboard.initialize();
31
+
32
+ // Register as a service for use in routes
33
+ ctx.setService("dashboard", dashboard);
34
+
35
+ // Warm caches
36
+ await ctx.core.cache.set("config", await loadConfig());
37
+
38
+ // Start background tasks
39
+ ctx.core.cron.schedule("0 * * * *", async () => {
40
+ await ctx.plugins.reports.generateHourly();
41
+ });
42
+ });
43
+
44
+ await server.start();
45
+ ```
46
+
47
+ ### HookContext
48
+
49
+ The `onReady` callback receives a `HookContext` with:
50
+
51
+ ```typescript
52
+ interface HookContext {
53
+ /** Database instance (Kysely) */
54
+ db: Kysely<any>;
55
+
56
+ /** Core services */
57
+ core: {
58
+ logger: Logger;
59
+ cache: Cache;
60
+ events: Events;
61
+ cron: Cron;
62
+ jobs: Jobs;
63
+ sse: SSE;
64
+ rateLimiter: RateLimiter;
65
+ errors: Errors;
66
+ workflows: Workflows;
67
+ processes: Processes;
68
+ };
69
+
70
+ /** Plugin services */
71
+ plugins: Record<string, any>;
72
+
73
+ /** Server configuration */
74
+ config: Record<string, any>;
75
+
76
+ /** Custom registered services */
77
+ services: Record<string, any>;
78
+
79
+ /** Register a service at runtime */
80
+ setService: <T>(name: string, service: T) => void;
81
+ }
82
+ ```
83
+
84
+ ### Multiple onReady Handlers
85
+
86
+ You can register multiple handlers - they execute in registration order:
87
+
88
+ ```typescript
89
+ server.onReady(async (ctx) => {
90
+ // First: Initialize core dependencies
91
+ await initializeDatabase(ctx.db);
92
+ });
93
+
94
+ server.onReady(async (ctx) => {
95
+ // Second: Warm caches
96
+ await warmCaches(ctx);
97
+ });
98
+
99
+ server.onReady(async (ctx) => {
100
+ // Third: Start background jobs
101
+ ctx.core.jobs.start();
102
+ });
103
+ ```
104
+
105
+ ## onShutdown Hook
106
+
107
+ Called when the server is shutting down. Use for cleanup operations.
108
+
109
+ ```typescript
110
+ server.onShutdown(async () => {
111
+ console.log("Server shutting down...");
112
+
113
+ // Close external connections
114
+ await externalApi.disconnect();
115
+
116
+ // Flush pending data
117
+ await analytics.flush();
118
+
119
+ // Save state
120
+ await saveCheckpoint();
121
+ });
122
+ ```
123
+
124
+ ### Graceful Shutdown
125
+
126
+ Enable automatic graceful shutdown on SIGTERM/SIGINT:
127
+
128
+ ```typescript
129
+ server
130
+ .onShutdown(async () => {
131
+ await cleanup();
132
+ })
133
+ .enableGracefulShutdown(); // Handles SIGTERM and SIGINT
134
+
135
+ await server.start();
136
+ ```
137
+
138
+ With `enableGracefulShutdown()`:
139
+ 1. SIGTERM/SIGINT triggers shutdown
140
+ 2. Server stops accepting new requests
141
+ 3. Running requests complete (with timeout)
142
+ 4. `onShutdown` handlers execute
143
+ 5. Core services shut down (jobs, cron, SSE, processes)
144
+ 6. Process exits
145
+
146
+ ### Manual Shutdown
147
+
148
+ You can also trigger shutdown programmatically:
149
+
150
+ ```typescript
151
+ // Somewhere in your code
152
+ await server.shutdown();
153
+ ```
154
+
155
+ ## onError Hook
156
+
157
+ Called when an unhandled error occurs during request handling or in background tasks.
158
+
159
+ ```typescript
160
+ server.onError(async (error, ctx) => {
161
+ // Log the error
162
+ ctx?.core.logger.error("Unhandled error", {
163
+ message: error.message,
164
+ stack: error.stack,
165
+ });
166
+
167
+ // Send to error tracking service
168
+ await errorTracker.capture(error, {
169
+ tags: { environment: process.env.NODE_ENV },
170
+ });
171
+
172
+ // Alert on critical errors
173
+ if (isCritical(error)) {
174
+ await ctx?.plugins.notifications.sendAlert({
175
+ channel: "ops",
176
+ message: `Critical error: ${error.message}`,
177
+ });
178
+ }
179
+ });
180
+ ```
181
+
182
+ **Note:** `ctx` may be undefined if the error occurs outside of a request context.
183
+
184
+ ## SvelteKit Adapter Usage
185
+
186
+ Lifecycle hooks are especially useful with the SvelteKit adapter where you don't call `server.start()` directly:
187
+
188
+ ```typescript
189
+ // src/server/index.ts
190
+ import { AppServer } from "@donkeylabs/server";
191
+ import { db } from "./db";
192
+
193
+ export const server = new AppServer({ db })
194
+ .use(authPlugin)
195
+ .use(usersPlugin)
196
+ .router(usersRouter)
197
+
198
+ // Initialize app-specific services after plugins are ready
199
+ .onReady(async (ctx) => {
200
+ // This runs when SvelteKit starts
201
+ const nvr = new NVR(ctx.plugins.auth);
202
+ await nvr.connect();
203
+ ctx.setService("nvr", nvr);
204
+ })
205
+
206
+ // Cleanup when SvelteKit stops
207
+ .onShutdown(async () => {
208
+ await cleanup();
209
+ })
210
+
211
+ // Handle errors
212
+ .onError(async (error, ctx) => {
213
+ await reportError(error);
214
+ });
215
+
216
+ // Export for SvelteKit adapter
217
+ export type AppContext = typeof server extends AppServer<infer C> ? C : never;
218
+ ```
219
+
220
+ ## Complete Example
221
+
222
+ ```typescript
223
+ import { AppServer, defineService } from "@donkeylabs/server";
224
+
225
+ // Define services
226
+ const cacheWarmerService = defineService("cacheWarmer", (ctx) => ({
227
+ warm: async () => {
228
+ const users = await ctx.db.selectFrom("users").selectAll().execute();
229
+ for (const user of users) {
230
+ await ctx.core.cache.set(`user:${user.id}`, user, 3600000);
231
+ }
232
+ },
233
+ }));
234
+
235
+ // Create server
236
+ const server = new AppServer({
237
+ db,
238
+ port: 3000,
239
+ config: {
240
+ environment: process.env.NODE_ENV,
241
+ },
242
+ });
243
+
244
+ // Register plugins and services
245
+ server
246
+ .use(authPlugin)
247
+ .use(usersPlugin)
248
+ .registerService(cacheWarmerService);
249
+
250
+ // Lifecycle hooks
251
+ server.onReady(async (ctx) => {
252
+ ctx.core.logger.info("Server ready", {
253
+ port: 3000,
254
+ environment: ctx.config.environment,
255
+ });
256
+
257
+ // Warm caches on startup
258
+ await ctx.services.cacheWarmer.warm();
259
+
260
+ // Schedule periodic cache warming
261
+ ctx.core.cron.schedule("*/30 * * * *", async () => {
262
+ await ctx.services.cacheWarmer.warm();
263
+ });
264
+ });
265
+
266
+ server.onShutdown(async () => {
267
+ console.log("Graceful shutdown initiated");
268
+ // Cleanup happens automatically for core services
269
+ });
270
+
271
+ server.onError(async (error, ctx) => {
272
+ console.error("Unhandled error:", error);
273
+ // Report to monitoring service
274
+ });
275
+
276
+ // Enable graceful shutdown and start
277
+ server.enableGracefulShutdown();
278
+ await server.start();
279
+
280
+ console.log("Server running on http://localhost:3000");
281
+ ```
282
+
283
+ ## Best Practices
284
+
285
+ ### 1. Keep onReady Fast
286
+ Don't block startup with heavy operations:
287
+
288
+ ```typescript
289
+ // Good - async initialization
290
+ server.onReady(async (ctx) => {
291
+ // Fire and forget for non-critical warmup
292
+ ctx.services.cacheWarmer.warm().catch(console.error);
293
+ });
294
+
295
+ // Avoid - blocking startup
296
+ server.onReady(async (ctx) => {
297
+ // This delays server readiness
298
+ await heavyInitialization(); // 30 seconds...
299
+ });
300
+ ```
301
+
302
+ ### 2. Handle Shutdown Timeouts
303
+ Don't let shutdown hang indefinitely:
304
+
305
+ ```typescript
306
+ server.onShutdown(async () => {
307
+ const timeout = setTimeout(() => {
308
+ console.error("Shutdown timeout - forcing exit");
309
+ process.exit(1);
310
+ }, 30000);
311
+
312
+ try {
313
+ await gracefulCleanup();
314
+ } finally {
315
+ clearTimeout(timeout);
316
+ }
317
+ });
318
+ ```
319
+
320
+ ### 3. Order Matters
321
+ Hooks execute in registration order. Register dependencies first:
322
+
323
+ ```typescript
324
+ // First: Initialize the connection
325
+ server.onReady(async (ctx) => {
326
+ const conn = await createConnection();
327
+ ctx.setService("conn", conn);
328
+ });
329
+
330
+ // Second: Use the connection
331
+ server.onReady(async (ctx) => {
332
+ await ctx.services.conn.ping(); // conn is available
333
+ });
334
+ ```
335
+
336
+ ### 4. Error Handling in Hooks
337
+ Errors in hooks can prevent startup or cleanup:
338
+
339
+ ```typescript
340
+ server.onReady(async (ctx) => {
341
+ try {
342
+ await riskyOperation();
343
+ } catch (error) {
344
+ ctx.core.logger.error("Non-critical init failed", { error });
345
+ // Don't rethrow - allow server to start
346
+ }
347
+ });
348
+ ```
@@ -0,0 +1,256 @@
1
+ # Custom Services
2
+
3
+ Custom services allow you to register application-specific dependencies that integrate with the server's context system. Services are available in route handlers via `ctx.services` with full type inference.
4
+
5
+ ## Overview
6
+
7
+ Use custom services when you need to:
8
+ - Initialize app-specific classes that depend on plugins
9
+ - Share stateful instances across route handlers
10
+ - Integrate third-party SDKs with type safety
11
+ - Create domain-specific facades over core services
12
+
13
+ ## Defining a Service
14
+
15
+ Use `defineService()` to create a type-safe service definition:
16
+
17
+ ```typescript
18
+ // src/server/services/nvr.ts
19
+ import { defineService } from "@donkeylabs/server";
20
+ import { NVRClient } from "./nvr-client";
21
+
22
+ export const nvrService = defineService("nvr", async (ctx) => {
23
+ // Access plugins, db, core services during initialization
24
+ const nvr = new NVRClient({
25
+ authProvider: ctx.plugins.auth,
26
+ logger: ctx.core.logger,
27
+ });
28
+
29
+ await nvr.connect();
30
+ return nvr;
31
+ });
32
+ ```
33
+
34
+ The factory function receives `HookContext` which provides:
35
+ - `ctx.db` - Database instance (Kysely)
36
+ - `ctx.core` - Core services (logger, cache, events, jobs, etc.)
37
+ - `ctx.plugins` - Plugin services
38
+ - `ctx.config` - Server configuration
39
+ - `ctx.services` - Other registered services
40
+
41
+ ## Registering Services
42
+
43
+ Register services with the server before starting:
44
+
45
+ ```typescript
46
+ // src/server/index.ts
47
+ import { AppServer } from "@donkeylabs/server";
48
+ import { nvrService } from "./services/nvr";
49
+ import { analyticsService } from "./services/analytics";
50
+
51
+ const server = new AppServer({ db, port: 3000 });
52
+
53
+ // Register using service definition (recommended)
54
+ server.registerService(nvrService);
55
+ server.registerService(analyticsService);
56
+
57
+ // Or register inline
58
+ server.registerService("cache-warmer", async (ctx) => {
59
+ return new CacheWarmer(ctx.core.cache);
60
+ });
61
+
62
+ await server.start();
63
+ ```
64
+
65
+ ## Using Services in Routes
66
+
67
+ Services are available via `ctx.services` in route handlers:
68
+
69
+ ```typescript
70
+ router.route("recordings").typed({
71
+ input: z.object({ cameraId: z.string() }),
72
+ output: recordingsSchema,
73
+ handle: async (input, ctx) => {
74
+ // Fully typed - ctx.services.nvr has proper type inference
75
+ return ctx.services.nvr.getRecordings(input.cameraId);
76
+ },
77
+ });
78
+ ```
79
+
80
+ ## Type Generation
81
+
82
+ When you run `donkeylabs generate`, the CLI scans for `defineService()` calls and generates types automatically. The generated `ServiceRegistry` interface includes all your services:
83
+
84
+ ```typescript
85
+ // Generated in context.d.ts
86
+ declare module "@donkeylabs/server" {
87
+ interface ServiceRegistry {
88
+ nvr: NVRClient;
89
+ analytics: AnalyticsService;
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### Service Definition Locations
95
+
96
+ The CLI scans these locations for service definitions:
97
+ - `src/server/services/*.ts`
98
+ - `src/lib/services/*.ts`
99
+ - Server entry file (e.g., `src/server/index.ts`)
100
+
101
+ ## Runtime Registration
102
+
103
+ You can also register services at runtime in `onReady` hooks:
104
+
105
+ ```typescript
106
+ server.onReady(async (ctx) => {
107
+ // Initialize something that needs the full context
108
+ const dashboard = new AdminDashboard(ctx.plugins, ctx.core);
109
+ await dashboard.initialize();
110
+
111
+ // Register it as a service
112
+ ctx.setService("dashboard", dashboard);
113
+ });
114
+ ```
115
+
116
+ Services registered via `setService()` are immediately available but won't have generated types (use `defineService()` for type generation).
117
+
118
+ ## Service Dependencies
119
+
120
+ Services can depend on other services by accessing them in the factory:
121
+
122
+ ```typescript
123
+ export const reportService = defineService("reports", async (ctx) => {
124
+ // Depend on another service (must be registered first)
125
+ const analytics = ctx.services.analytics;
126
+
127
+ return new ReportGenerator(analytics, ctx.core.cache);
128
+ });
129
+ ```
130
+
131
+ **Important:** Register services in dependency order. If service B depends on service A, register A first.
132
+
133
+ ## Best Practices
134
+
135
+ ### 1. Keep Services Focused
136
+ Each service should have a single responsibility:
137
+
138
+ ```typescript
139
+ // Good - focused service
140
+ export const emailService = defineService("email", (ctx) => ({
141
+ send: (to, subject, body) => sendEmail(to, subject, body),
142
+ sendTemplate: (to, template, data) => sendTemplate(to, template, data),
143
+ }));
144
+
145
+ // Avoid - too many responsibilities
146
+ export const everythingService = defineService("everything", (ctx) => ({
147
+ sendEmail: ...,
148
+ processPayment: ...,
149
+ generateReport: ...,
150
+ }));
151
+ ```
152
+
153
+ ### 2. Handle Cleanup
154
+ If your service needs cleanup, use `onShutdown`:
155
+
156
+ ```typescript
157
+ export const connectionPoolService = defineService("pool", async (ctx) => {
158
+ const pool = await createPool();
159
+
160
+ // Register cleanup
161
+ // Note: You'll need to handle this in your server setup
162
+ return pool;
163
+ });
164
+
165
+ // In server setup
166
+ server.registerService(connectionPoolService);
167
+ server.onShutdown(async () => {
168
+ await server.getServices().pool?.close();
169
+ });
170
+ ```
171
+
172
+ ### 3. Use Plugins for Reusable Logic
173
+ Services are app-specific. For reusable business logic, use plugins instead:
174
+
175
+ ```typescript
176
+ // Use a plugin for reusable auth logic
177
+ export const authPlugin = createPlugin.define({
178
+ name: "auth",
179
+ service: (ctx) => ({ ... }),
180
+ });
181
+
182
+ // Use a service for app-specific integrations
183
+ export const myAppService = defineService("myApp", (ctx) => {
184
+ // Uses the auth plugin
185
+ return new MyAppIntegration(ctx.plugins.auth);
186
+ });
187
+ ```
188
+
189
+ ## Example: Full Service Setup
190
+
191
+ ```typescript
192
+ // services/analytics.ts
193
+ import { defineService } from "@donkeylabs/server";
194
+ import { AnalyticsSDK } from "analytics-sdk";
195
+
196
+ export class AnalyticsService {
197
+ private sdk: AnalyticsSDK;
198
+
199
+ constructor(apiKey: string, logger: Logger) {
200
+ this.sdk = new AnalyticsSDK(apiKey);
201
+ this.logger = logger;
202
+ }
203
+
204
+ track(event: string, properties: Record<string, any>) {
205
+ this.logger.debug("Tracking event", { event, properties });
206
+ return this.sdk.track(event, properties);
207
+ }
208
+
209
+ identify(userId: string, traits: Record<string, any>) {
210
+ return this.sdk.identify(userId, traits);
211
+ }
212
+ }
213
+
214
+ export const analyticsService = defineService("analytics", (ctx) => {
215
+ const apiKey = ctx.config.analyticsApiKey;
216
+ if (!apiKey) {
217
+ ctx.core.logger.warn("Analytics API key not configured");
218
+ return null;
219
+ }
220
+
221
+ return new AnalyticsService(apiKey, ctx.core.logger);
222
+ });
223
+ ```
224
+
225
+ ```typescript
226
+ // server/index.ts
227
+ import { AppServer } from "@donkeylabs/server";
228
+ import { analyticsService } from "./services/analytics";
229
+
230
+ const server = new AppServer({
231
+ db,
232
+ port: 3000,
233
+ config: {
234
+ analyticsApiKey: process.env.ANALYTICS_API_KEY,
235
+ },
236
+ });
237
+
238
+ server.registerService(analyticsService);
239
+
240
+ // Use in routes
241
+ router.route("signup").typed({
242
+ input: signupSchema,
243
+ output: userSchema,
244
+ handle: async (input, ctx) => {
245
+ const user = await ctx.plugins.users.create(input);
246
+
247
+ // Track signup event
248
+ ctx.services.analytics?.track("user.signup", {
249
+ userId: user.id,
250
+ email: user.email,
251
+ });
252
+
253
+ return user;
254
+ },
255
+ });
256
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",