@donkeylabs/server 1.1.17 → 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 +6 -2
- package/docs/core-services.md +10 -0
- package/docs/lifecycle-hooks.md +348 -0
- package/docs/processes.md +531 -0
- package/docs/services.md +256 -0
- package/package.json +5 -1
- package/src/core/audit.ts +4 -4
- package/src/core/index.ts +9 -0
- package/src/core/job-adapter-kysely.ts +4 -4
- package/src/core/process-client.ts +349 -0
- package/src/core/processes.ts +45 -2
- package/src/core/workflow-adapter-kysely.ts +4 -4
- package/src/core.ts +22 -0
- package/src/index.ts +18 -1
- package/src/process-client.ts +19 -0
- package/src/server.ts +343 -5
package/CLAUDE.md
CHANGED
|
@@ -130,7 +130,7 @@ await api.users.create({ email, name });
|
|
|
130
130
|
| `.html()` | htmx partials |
|
|
131
131
|
|
|
132
132
|
## Core Services (ctx.core)
|
|
133
|
-
`ctx.core.logger`, `ctx.core.cache`, `ctx.core.jobs`, `ctx.core.events`, `ctx.core.rateLimiter`, `ctx.core.sse`
|
|
133
|
+
`ctx.core.logger`, `ctx.core.cache`, `ctx.core.jobs`, `ctx.core.events`, `ctx.core.rateLimiter`, `ctx.core.sse`, `ctx.core.processes`, `ctx.core.workflows`
|
|
134
134
|
|
|
135
135
|
## Error Handling
|
|
136
136
|
```ts
|
|
@@ -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
|
-
|
|
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
|
package/docs/core-services.md
CHANGED
|
@@ -11,6 +11,9 @@ Core services are foundational utilities automatically available to all plugins
|
|
|
11
11
|
| [Events](events.md) | Pub/sub event system | In-memory |
|
|
12
12
|
| [Cron](cron.md) | Scheduled recurring tasks | In-memory |
|
|
13
13
|
| [Jobs](jobs.md) | Background job queue | In-memory |
|
|
14
|
+
| [External Jobs](external-jobs.md) | Jobs in any language | SQLite |
|
|
15
|
+
| [Processes](processes.md) | Long-running daemons | SQLite |
|
|
16
|
+
| [Workflows](workflows.md) | Multi-step orchestration | In-memory |
|
|
14
17
|
| [SSE](sse.md) | Server-Sent Events | In-memory |
|
|
15
18
|
| [RateLimiter](rate-limiter.md) | Request throttling | In-memory |
|
|
16
19
|
| [Errors](errors.md) | HTTP error factories | - |
|
|
@@ -336,3 +339,10 @@ See individual service documentation for adapter interfaces:
|
|
|
336
339
|
- [Events Adapters](events.md#custom-adapters)
|
|
337
340
|
- [Jobs Adapters](jobs.md#custom-adapters)
|
|
338
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
|
+
```
|