@donkeylabs/server 0.1.0 → 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/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
+ ```