@donkeylabs/server 0.1.1 → 0.1.2
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/cli/donkeylabs +6 -0
- package/context.d.ts +17 -0
- package/docs/api-client.md +520 -0
- package/docs/cache.md +437 -0
- package/docs/cli.md +353 -0
- package/docs/core-services.md +338 -0
- package/docs/cron.md +465 -0
- package/docs/errors.md +303 -0
- package/docs/events.md +460 -0
- package/docs/handlers.md +549 -0
- package/docs/jobs.md +556 -0
- package/docs/logger.md +316 -0
- package/docs/middleware.md +682 -0
- package/docs/plugins.md +524 -0
- package/docs/project-structure.md +493 -0
- package/docs/rate-limiter.md +525 -0
- package/docs/router.md +566 -0
- package/docs/sse.md +542 -0
- package/docs/svelte-frontend.md +324 -0
- package/package.json +12 -9
- package/registry.d.ts +11 -0
- package/src/index.ts +1 -1
- package/src/server.ts +1 -0
package/docs/cron.md
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# Cron Service
|
|
2
|
+
|
|
3
|
+
Schedule recurring tasks using cron expressions. Supports standard 5-field and extended 6-field formats.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// Run every minute
|
|
9
|
+
ctx.core.cron.schedule("* * * * *", () => {
|
|
10
|
+
console.log("Every minute!");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Run at midnight daily
|
|
14
|
+
ctx.core.cron.schedule("0 0 * * *", async () => {
|
|
15
|
+
await generateDailyReport();
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## API Reference
|
|
22
|
+
|
|
23
|
+
### Interface
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
interface Cron {
|
|
27
|
+
schedule(
|
|
28
|
+
expression: string,
|
|
29
|
+
handler: () => void | Promise<void>,
|
|
30
|
+
options?: { name?: string; enabled?: boolean }
|
|
31
|
+
): string;
|
|
32
|
+
unschedule(taskId: string): boolean;
|
|
33
|
+
pause(taskId: string): void;
|
|
34
|
+
resume(taskId: string): void;
|
|
35
|
+
list(): CronTask[];
|
|
36
|
+
get(taskId: string): CronTask | undefined;
|
|
37
|
+
trigger(taskId: string): Promise<void>;
|
|
38
|
+
start(): void;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CronTask {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
expression: string;
|
|
46
|
+
handler: () => void | Promise<void>;
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
lastRun?: Date;
|
|
49
|
+
nextRun?: Date;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Methods
|
|
54
|
+
|
|
55
|
+
| Method | Description |
|
|
56
|
+
|--------|-------------|
|
|
57
|
+
| `schedule(expr, handler, opts?)` | Create scheduled task, returns task ID |
|
|
58
|
+
| `unschedule(taskId)` | Remove task permanently |
|
|
59
|
+
| `pause(taskId)` | Temporarily disable task |
|
|
60
|
+
| `resume(taskId)` | Re-enable paused task |
|
|
61
|
+
| `list()` | Get all scheduled tasks |
|
|
62
|
+
| `get(taskId)` | Get specific task |
|
|
63
|
+
| `trigger(taskId)` | Execute task immediately |
|
|
64
|
+
| `start()` | Start the scheduler |
|
|
65
|
+
| `stop()` | Stop the scheduler |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Cron Expression Format
|
|
70
|
+
|
|
71
|
+
### 5-Field Format (Standard)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
┌───────────── minute (0-59)
|
|
75
|
+
│ ┌───────────── hour (0-23)
|
|
76
|
+
│ │ ┌───────────── day of month (1-31)
|
|
77
|
+
│ │ │ ┌───────────── month (1-12)
|
|
78
|
+
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
|
|
79
|
+
│ │ │ │ │
|
|
80
|
+
* * * * *
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 6-Field Format (With Seconds)
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
┌───────────── second (0-59)
|
|
87
|
+
│ ┌───────────── minute (0-59)
|
|
88
|
+
│ │ ┌───────────── hour (0-23)
|
|
89
|
+
│ │ │ ┌───────────── day of month (1-31)
|
|
90
|
+
│ │ │ │ ┌───────────── month (1-12)
|
|
91
|
+
│ │ │ │ │ ┌───────────── day of week (0-6)
|
|
92
|
+
│ │ │ │ │ │
|
|
93
|
+
* * * * * *
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Special Characters
|
|
97
|
+
|
|
98
|
+
| Character | Description | Example |
|
|
99
|
+
|-----------|-------------|---------|
|
|
100
|
+
| `*` | Any value | `* * * * *` (every minute) |
|
|
101
|
+
| `,` | Value list | `1,15,30 * * * *` (minutes 1, 15, 30) |
|
|
102
|
+
| `-` | Range | `9-17 * * * *` (hours 9 through 17) |
|
|
103
|
+
| `/` | Step | `*/5 * * * *` (every 5 minutes) |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Common Patterns
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// Every minute
|
|
111
|
+
"* * * * *"
|
|
112
|
+
|
|
113
|
+
// Every 5 minutes
|
|
114
|
+
"*/5 * * * *"
|
|
115
|
+
|
|
116
|
+
// Every hour at minute 0
|
|
117
|
+
"0 * * * *"
|
|
118
|
+
|
|
119
|
+
// Every day at midnight
|
|
120
|
+
"0 0 * * *"
|
|
121
|
+
|
|
122
|
+
// Every day at 9am
|
|
123
|
+
"0 9 * * *"
|
|
124
|
+
|
|
125
|
+
// Every Monday at 9am
|
|
126
|
+
"0 9 * * 1"
|
|
127
|
+
|
|
128
|
+
// Every weekday at 9am
|
|
129
|
+
"0 9 * * 1-5"
|
|
130
|
+
|
|
131
|
+
// First day of month at midnight
|
|
132
|
+
"0 0 1 * *"
|
|
133
|
+
|
|
134
|
+
// Every 30 seconds (6-field)
|
|
135
|
+
"*/30 * * * * *"
|
|
136
|
+
|
|
137
|
+
// Every hour, Monday-Friday, 9am-5pm
|
|
138
|
+
"0 9-17 * * 1-5"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Usage Examples
|
|
144
|
+
|
|
145
|
+
### Basic Scheduling
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Schedule with auto-generated ID
|
|
149
|
+
const taskId = ctx.core.cron.schedule("0 * * * *", () => {
|
|
150
|
+
console.log("Hourly task");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Schedule with name for easier management
|
|
154
|
+
ctx.core.cron.schedule("0 0 * * *", async () => {
|
|
155
|
+
await generateReport();
|
|
156
|
+
}, { name: "daily-report" });
|
|
157
|
+
|
|
158
|
+
// Schedule but start disabled
|
|
159
|
+
ctx.core.cron.schedule("*/5 * * * *", () => {
|
|
160
|
+
console.log("This won't run until enabled");
|
|
161
|
+
}, { name: "optional-task", enabled: false });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Managing Tasks
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// List all tasks
|
|
168
|
+
const tasks = ctx.core.cron.list();
|
|
169
|
+
for (const task of tasks) {
|
|
170
|
+
console.log(`${task.name}: ${task.expression} (${task.enabled ? "active" : "paused"})`);
|
|
171
|
+
console.log(` Last run: ${task.lastRun}`);
|
|
172
|
+
console.log(` Next run: ${task.nextRun}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Pause a task
|
|
176
|
+
ctx.core.cron.pause(taskId);
|
|
177
|
+
|
|
178
|
+
// Resume a task
|
|
179
|
+
ctx.core.cron.resume(taskId);
|
|
180
|
+
|
|
181
|
+
// Remove a task
|
|
182
|
+
ctx.core.cron.unschedule(taskId);
|
|
183
|
+
|
|
184
|
+
// Trigger immediately (for testing)
|
|
185
|
+
await ctx.core.cron.trigger(taskId);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Real-World Examples
|
|
191
|
+
|
|
192
|
+
### Daily Reports
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// plugins/analytics/index.ts
|
|
196
|
+
service: async (ctx) => {
|
|
197
|
+
// Generate daily analytics at 1am
|
|
198
|
+
ctx.core.cron.schedule("0 1 * * *", async () => {
|
|
199
|
+
ctx.core.logger.info("Generating daily analytics report");
|
|
200
|
+
|
|
201
|
+
const yesterday = new Date();
|
|
202
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
203
|
+
|
|
204
|
+
const stats = await ctx.db
|
|
205
|
+
.selectFrom("events")
|
|
206
|
+
.select([
|
|
207
|
+
ctx.db.fn.count("id").as("totalEvents"),
|
|
208
|
+
ctx.db.fn.countDistinct("userId").as("uniqueUsers"),
|
|
209
|
+
])
|
|
210
|
+
.where("createdAt", ">=", yesterday.toISOString())
|
|
211
|
+
.executeTakeFirst();
|
|
212
|
+
|
|
213
|
+
await ctx.db.insertInto("daily_reports").values({
|
|
214
|
+
date: yesterday.toISOString().split("T")[0],
|
|
215
|
+
data: JSON.stringify(stats),
|
|
216
|
+
}).execute();
|
|
217
|
+
|
|
218
|
+
ctx.core.logger.info("Daily report generated", stats);
|
|
219
|
+
}, { name: "daily-analytics" });
|
|
220
|
+
|
|
221
|
+
return { /* analytics methods */ };
|
|
222
|
+
};
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Cache Cleanup
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// Clean expired cache entries every hour
|
|
229
|
+
ctx.core.cron.schedule("0 * * * *", async () => {
|
|
230
|
+
const expiredKeys = await ctx.core.cache.keys("temp:*");
|
|
231
|
+
let cleaned = 0;
|
|
232
|
+
|
|
233
|
+
for (const key of expiredKeys) {
|
|
234
|
+
if (!(await ctx.core.cache.has(key))) {
|
|
235
|
+
cleaned++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
ctx.core.logger.debug("Cache cleanup", { checked: expiredKeys.length, cleaned });
|
|
240
|
+
}, { name: "cache-cleanup" });
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Health Checks
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
// Check external service health every 5 minutes
|
|
247
|
+
ctx.core.cron.schedule("*/5 * * * *", async () => {
|
|
248
|
+
const services = ["api.stripe.com", "api.sendgrid.com", "s3.amazonaws.com"];
|
|
249
|
+
|
|
250
|
+
for (const service of services) {
|
|
251
|
+
try {
|
|
252
|
+
const start = Date.now();
|
|
253
|
+
const response = await fetch(`https://${service}/health`);
|
|
254
|
+
const latency = Date.now() - start;
|
|
255
|
+
|
|
256
|
+
if (response.ok) {
|
|
257
|
+
ctx.core.logger.debug("Health check passed", { service, latency });
|
|
258
|
+
} else {
|
|
259
|
+
ctx.core.logger.warn("Health check degraded", { service, status: response.status });
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
ctx.core.logger.error("Health check failed", { service, error: error.message });
|
|
263
|
+
await ctx.core.events.emit("health.check.failed", { service, error: error.message });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, { name: "health-checks" });
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Data Synchronization
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
// Sync data from external API every 15 minutes
|
|
273
|
+
ctx.core.cron.schedule("*/15 * * * *", async () => {
|
|
274
|
+
ctx.core.logger.info("Starting data sync");
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch("https://api.external.com/products");
|
|
278
|
+
const products = await response.json();
|
|
279
|
+
|
|
280
|
+
for (const product of products) {
|
|
281
|
+
await ctx.db
|
|
282
|
+
.insertInto("products")
|
|
283
|
+
.values(product)
|
|
284
|
+
.onConflict((oc) => oc.column("externalId").doUpdateSet(product))
|
|
285
|
+
.execute();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
ctx.core.logger.info("Data sync completed", { count: products.length });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
ctx.core.logger.error("Data sync failed", { error: error.message });
|
|
291
|
+
}
|
|
292
|
+
}, { name: "product-sync" });
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Scheduled Notifications
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
// Send weekly digest every Monday at 9am
|
|
299
|
+
ctx.core.cron.schedule("0 9 * * 1", async () => {
|
|
300
|
+
const users = await ctx.db
|
|
301
|
+
.selectFrom("users")
|
|
302
|
+
.where("weeklyDigest", "=", true)
|
|
303
|
+
.execute();
|
|
304
|
+
|
|
305
|
+
for (const user of users) {
|
|
306
|
+
await ctx.core.jobs.enqueue("sendWeeklyDigest", {
|
|
307
|
+
userId: user.id,
|
|
308
|
+
email: user.email,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ctx.core.logger.info("Weekly digest queued", { users: users.length });
|
|
313
|
+
}, { name: "weekly-digest" });
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Error Handling
|
|
319
|
+
|
|
320
|
+
Task errors are logged but don't stop other tasks:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
ctx.core.cron.schedule("* * * * *", async () => {
|
|
324
|
+
throw new Error("Task failed");
|
|
325
|
+
});
|
|
326
|
+
// Error is logged: [Cron] Task "cron_1_..." failed: Task failed
|
|
327
|
+
// Task continues to run on next schedule
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
For critical tasks, implement retry logic:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
ctx.core.cron.schedule("0 * * * *", async () => {
|
|
334
|
+
const maxRetries = 3;
|
|
335
|
+
|
|
336
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
337
|
+
try {
|
|
338
|
+
await criticalOperation();
|
|
339
|
+
return; // Success
|
|
340
|
+
} catch (error) {
|
|
341
|
+
ctx.core.logger.warn("Task attempt failed", { attempt, error: error.message });
|
|
342
|
+
|
|
343
|
+
if (attempt === maxRetries) {
|
|
344
|
+
ctx.core.logger.error("Task failed after retries", { maxRetries });
|
|
345
|
+
await ctx.core.events.emit("cron.task.failed", {
|
|
346
|
+
task: "critical-operation",
|
|
347
|
+
error: error.message,
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
await new Promise((r) => setTimeout(r, 1000 * attempt)); // Backoff
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}, { name: "critical-operation" });
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Testing Cron Tasks
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { createCron } from "./core/cron";
|
|
363
|
+
|
|
364
|
+
describe("Cron Tasks", () => {
|
|
365
|
+
it("should execute task on trigger", async () => {
|
|
366
|
+
const cron = createCron();
|
|
367
|
+
let executed = false;
|
|
368
|
+
|
|
369
|
+
const taskId = cron.schedule("0 0 1 1 *", () => {
|
|
370
|
+
executed = true;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Manually trigger instead of waiting
|
|
374
|
+
await cron.trigger(taskId);
|
|
375
|
+
|
|
376
|
+
expect(executed).toBe(true);
|
|
377
|
+
|
|
378
|
+
await cron.stop();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Best Practices
|
|
386
|
+
|
|
387
|
+
### 1. Name Your Tasks
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// Good - identifiable in logs and list()
|
|
391
|
+
ctx.core.cron.schedule("0 * * * *", handler, { name: "hourly-cleanup" });
|
|
392
|
+
|
|
393
|
+
// Bad - auto-generated IDs are hard to track
|
|
394
|
+
ctx.core.cron.schedule("0 * * * *", handler);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### 2. Log Task Execution
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
ctx.core.cron.schedule("0 0 * * *", async () => {
|
|
401
|
+
const start = Date.now();
|
|
402
|
+
ctx.core.logger.info("Daily task starting");
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
await processDaily();
|
|
406
|
+
ctx.core.logger.info("Daily task completed", { duration: Date.now() - start });
|
|
407
|
+
} catch (error) {
|
|
408
|
+
ctx.core.logger.error("Daily task failed", { error: error.message });
|
|
409
|
+
}
|
|
410
|
+
}, { name: "daily-process" });
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### 3. Use Jobs for Heavy Work
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
// Good - cron schedules, jobs process
|
|
417
|
+
ctx.core.cron.schedule("0 0 * * *", async () => {
|
|
418
|
+
const users = await ctx.db.selectFrom("users").execute();
|
|
419
|
+
|
|
420
|
+
for (const user of users) {
|
|
421
|
+
// Queue each as separate job
|
|
422
|
+
await ctx.core.jobs.enqueue("processUser", { userId: user.id });
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Bad - long-running cron task
|
|
427
|
+
ctx.core.cron.schedule("0 0 * * *", async () => {
|
|
428
|
+
const users = await ctx.db.selectFrom("users").execute();
|
|
429
|
+
|
|
430
|
+
for (const user of users) {
|
|
431
|
+
await heavyProcessing(user); // Blocks cron
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 4. Consider Time Zones
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// Be explicit about timing expectations
|
|
440
|
+
// This runs at midnight server time
|
|
441
|
+
ctx.core.cron.schedule("0 0 * * *", handler, { name: "midnight-task" });
|
|
442
|
+
|
|
443
|
+
// Document if specific timezone is needed
|
|
444
|
+
// TODO: Runs at midnight UTC - adjust for local time if needed
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### 5. Monitor Task Health
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
ctx.core.cron.schedule("*/5 * * * *", async () => {
|
|
451
|
+
// Emit metrics for monitoring
|
|
452
|
+
const tasks = ctx.core.cron.list();
|
|
453
|
+
|
|
454
|
+
for (const task of tasks) {
|
|
455
|
+
if (task.lastRun) {
|
|
456
|
+
const timeSinceRun = Date.now() - task.lastRun.getTime();
|
|
457
|
+
await ctx.core.events.emit("cron.task.metric", {
|
|
458
|
+
name: task.name,
|
|
459
|
+
enabled: task.enabled,
|
|
460
|
+
timeSinceLastRun: timeSinceRun,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}, { name: "cron-monitor" });
|
|
465
|
+
```
|