@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 +5 -1
- package/docs/core-services.md +7 -0
- package/docs/lifecycle-hooks.md +348 -0
- package/docs/services.md +256 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
@@ -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
|
+
```
|
package/docs/services.md
ADDED
|
@@ -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
|
+
```
|