@donkeylabs/server 2.0.23 → 2.0.24

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 CHANGED
@@ -26,7 +26,7 @@ ctx.core.cron.schedule("0 0 * * *", async () => {
26
26
  interface Cron {
27
27
  schedule(
28
28
  expression: string,
29
- handler: () => void | Promise<void>,
29
+ handler: (logger?: Logger, ctx?: CronRunContext) => void | Promise<void>,
30
30
  options?: { name?: string; enabled?: boolean }
31
31
  ): string;
32
32
  unschedule(taskId: string): boolean;
@@ -43,11 +43,19 @@ interface CronTask {
43
43
  id: string;
44
44
  name: string;
45
45
  expression: string;
46
- handler: () => void | Promise<void>;
46
+ handler: (logger?: Logger, ctx?: CronRunContext) => void | Promise<void>;
47
47
  enabled: boolean;
48
48
  lastRun?: Date;
49
49
  nextRun?: Date;
50
50
  }
51
+
52
+ interface CronRunContext {
53
+ taskId: string;
54
+ name: string;
55
+ logger?: Logger;
56
+ emit?: (event: string, data?: Record<string, any>) => Promise<void>;
57
+ log?: (level: LogLevel, message: string, data?: Record<string, any>) => void;
58
+ }
51
59
  ```
52
60
 
53
61
  ### Methods
@@ -356,6 +364,24 @@ ctx.core.cron.schedule("0 * * * *", async () => {
356
364
 
357
365
  ---
358
366
 
367
+ ## Logs and Custom Events
368
+
369
+ Cron handlers receive a scoped logger and a context helper. Logs are persisted and emitted as events:
370
+
371
+ - `log.cron` (all cron logs)
372
+ - `log.cron.<taskName>` (per task)
373
+
374
+ You can also emit custom events scoped to a cron task:
375
+
376
+ ```ts
377
+ ctx.core.cron.schedule("0 * * * *", async (logger, ctx) => {
378
+ ctx?.log?.("info", "Hourly cleanup started");
379
+ await ctx?.emit?.("cleanup.started", { when: new Date().toISOString() });
380
+ });
381
+ ```
382
+
383
+ ---
384
+
359
385
  ## Testing Cron Tasks
360
386
 
361
387
  ```ts
package/docs/jobs.md CHANGED
@@ -438,6 +438,21 @@ interface JobAdapter {
438
438
  }
439
439
  ```
440
440
 
441
+ ---
442
+
443
+ ## Logs and Custom Events
444
+
445
+ Job handlers receive a scoped logger and helpers. Logs are persisted and emitted as events:
446
+
447
+ - `log.job` (all job logs)
448
+ - `log.job.<jobId>` (per job)
449
+
450
+ Custom events are emitted via `ctx.emit`:
451
+
452
+ - `job.event`
453
+ - `job.<jobName>.event`
454
+ - `job.<jobId>.event`
455
+
441
456
  ### SQLite Adapter Example
442
457
 
443
458
  ```ts
package/docs/workflows.md CHANGED
@@ -518,6 +518,31 @@ export const reportWorkflow = workflow("report.generate")
518
518
  .build();
519
519
  ```
520
520
 
521
+ #### Workflow Logs and Custom Events
522
+
523
+ Workflow handlers get a scoped logger and helpers:
524
+
525
+ - `ctx.logger` and `ctx.core.logger` are scoped to `source=workflow`, `sourceId=<instanceId>`
526
+ - Logs are persisted and emitted as events:
527
+ - `log.workflow` (all workflow logs)
528
+ - `log.workflow.<instanceId>` (per workflow instance)
529
+
530
+ Custom events can be emitted with `ctx.emit`:
531
+
532
+ ```typescript
533
+ export const reportWorkflow = workflow("report.generate")
534
+ .task("run", {
535
+ handler: async (input, ctx) => {
536
+ ctx.log?.("info", "Starting report", { reportId: input.reportId });
537
+ await ctx.emit?.("progress", { stage: "fetch" });
538
+ const data = await ctx.plugins.reports.generate(input.reportId);
539
+ await ctx.emit?.("progress", { stage: "write" });
540
+ return { data };
541
+ },
542
+ })
543
+ .build();
544
+ ```
545
+
521
546
  ### Inline Mode
522
547
 
523
548
  For lightweight workflows that complete quickly, you can opt into inline execution:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.23",
3
+ "version": "2.0.24",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
package/src/core/cron.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  // Core Cron Service
2
2
  // Schedule recurring tasks with cron expressions
3
3
 
4
- import type { Logger } from "./logger";
4
+ import type { Logger, LogLevel } from "./logger";
5
+ import type { Events } from "./events";
5
6
 
6
7
  export interface CronTask {
7
8
  id: string;
8
9
  name: string;
9
10
  expression: string;
10
- handler: (logger?: Logger) => void | Promise<void>;
11
+ handler: (logger?: Logger, ctx?: CronRunContext) => void | Promise<void>;
11
12
  enabled: boolean;
12
13
  lastRun?: Date;
13
14
  nextRun?: Date;
@@ -16,12 +17,13 @@ export interface CronTask {
16
17
  export interface CronConfig {
17
18
  timezone?: string; // For future use
18
19
  logger?: Logger;
20
+ events?: Events;
19
21
  }
20
22
 
21
23
  export interface Cron {
22
24
  schedule(
23
25
  expression: string,
24
- handler: (logger?: Logger) => void | Promise<void>,
26
+ handler: (logger?: Logger, ctx?: CronRunContext) => void | Promise<void>,
25
27
  options?: { name?: string; enabled?: boolean }
26
28
  ): string;
27
29
  unschedule(taskId: string): boolean;
@@ -34,6 +36,14 @@ export interface Cron {
34
36
  stop(): Promise<void>;
35
37
  }
36
38
 
39
+ export interface CronRunContext {
40
+ taskId: string;
41
+ name: string;
42
+ logger?: Logger;
43
+ emit?: (event: string, data?: Record<string, any>) => Promise<void>;
44
+ log?: (level: LogLevel, message: string, data?: Record<string, any>) => void;
45
+ }
46
+
37
47
  // Simple cron expression parser
38
48
  // Supports: * (any), specific values, ranges (1-5), steps (*/5)
39
49
  // Format: second minute hour dayOfMonth month dayOfWeek
@@ -263,14 +273,16 @@ class CronImpl implements Cron {
263
273
  private timer: ReturnType<typeof setInterval> | null = null;
264
274
  private taskCounter = 0;
265
275
  private logger?: Logger;
276
+ private events?: Events;
266
277
 
267
278
  constructor(config: CronConfig = {}) {
268
279
  this.logger = config.logger;
280
+ this.events = config.events;
269
281
  }
270
282
 
271
283
  schedule(
272
284
  expression: string,
273
- handler: (logger?: Logger) => void | Promise<void>,
285
+ handler: (logger?: Logger, ctx?: CronRunContext) => void | Promise<void>,
274
286
  options: { name?: string; enabled?: boolean } = {}
275
287
  ): string {
276
288
  const id = `cron_${++this.taskCounter}_${Date.now()}`;
@@ -341,7 +353,7 @@ class CronImpl implements Cron {
341
353
 
342
354
  task.lastRun = new Date();
343
355
  const scopedLogger = this.logger?.scoped("cron", task.name);
344
- await task.handler(scopedLogger);
356
+ await task.handler(scopedLogger, this.createRunContext(task, scopedLogger));
345
357
  }
346
358
 
347
359
  start(): void {
@@ -365,7 +377,7 @@ class CronImpl implements Cron {
365
377
 
366
378
  // Execute handler with scoped logger (fire and forget, but log errors)
367
379
  const scopedLogger = this.logger?.scoped("cron", task.name);
368
- Promise.resolve(task.handler(scopedLogger)).catch(err => {
380
+ Promise.resolve(task.handler(scopedLogger, this.createRunContext(task, scopedLogger))).catch(err => {
369
381
  console.error(`[Cron] Task "${task.name}" failed:`, err);
370
382
  });
371
383
  }
@@ -396,7 +408,7 @@ class CronImpl implements Cron {
396
408
 
397
409
  // Execute the handler asynchronously with scoped logger
398
410
  const scopedLogger = this.logger?.scoped("cron", task.name);
399
- Promise.resolve(task.handler(scopedLogger)).catch(err => {
411
+ Promise.resolve(task.handler(scopedLogger, this.createRunContext(task, scopedLogger))).catch(err => {
400
412
  console.error(`[Cron] Catch-up task "${task.name}" failed:`, err);
401
413
  });
402
414
 
@@ -424,6 +436,37 @@ class CronImpl implements Cron {
424
436
  this.timer = null;
425
437
  }
426
438
  }
439
+
440
+ private createRunContext(task: InternalCronTask, logger?: Logger): CronRunContext {
441
+ const emit = this.events
442
+ ? async (event: string, data?: Record<string, any>) => {
443
+ const payload = {
444
+ taskId: task.id,
445
+ name: task.name,
446
+ event,
447
+ data,
448
+ };
449
+
450
+ await this.events!.emit("cron.event", payload);
451
+ await this.events!.emit(`cron.${task.name}.event`, payload);
452
+ await this.events!.emit(`cron.${task.id}.event`, payload);
453
+ }
454
+ : undefined;
455
+
456
+ const log = logger
457
+ ? (level: LogLevel, message: string, data?: Record<string, any>) => {
458
+ logger[level](message, data);
459
+ }
460
+ : undefined;
461
+
462
+ return {
463
+ taskId: task.id,
464
+ name: task.name,
465
+ logger,
466
+ emit,
467
+ log,
468
+ };
469
+ }
427
470
  }
428
471
 
429
472
  export function createCron(config?: CronConfig): Cron {
package/src/core/index.ts CHANGED
@@ -32,6 +32,7 @@ export {
32
32
  export {
33
33
  type Cron,
34
34
  type CronTask,
35
+ type CronRunContext,
35
36
  type CronConfig,
36
37
  createCron,
37
38
  } from "./cron";
@@ -41,6 +42,7 @@ export {
41
42
  type Job,
42
43
  type JobStatus,
43
44
  type JobHandler,
45
+ type JobHandlerContext,
44
46
  type JobAdapter,
45
47
  type JobsConfig,
46
48
  type GetAllJobsOptions,
package/src/core/jobs.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Supports both in-process handlers and external processes (Python, Go, Shell, etc.)
4
4
 
5
5
  import type { Events } from "./events";
6
- import type { Logger } from "./logger";
6
+ import type { Logger, LogLevel } from "./logger";
7
7
  import type {
8
8
  ExternalJobConfig,
9
9
  ExternalJob,
@@ -59,7 +59,13 @@ export interface Job {
59
59
  }
60
60
 
61
61
  export interface JobHandler<T = any, R = any> {
62
- (data: T, ctx?: { logger: Logger }): Promise<R>;
62
+ (data: T, ctx?: JobHandlerContext): Promise<R>;
63
+ }
64
+
65
+ export interface JobHandlerContext {
66
+ logger?: Logger;
67
+ emit?: (event: string, data?: Record<string, any>) => Promise<void>;
68
+ log?: (level: LogLevel, message: string, data?: Record<string, any>) => void;
63
69
  }
64
70
 
65
71
  /** Options for listing all jobs */
@@ -999,6 +1005,7 @@ class JobsImpl implements Jobs {
999
1005
  ): void {
1000
1006
  const decoder = new TextDecoder();
1001
1007
  const events = this.events;
1008
+ const scopedLogger = this.logger?.scoped("job", jobId);
1002
1009
 
1003
1010
  // Helper to stream a ReadableStream
1004
1011
  const streamOutput = async (
@@ -1028,6 +1035,10 @@ class JobsImpl implements Jobs {
1028
1035
  message: text.trim(),
1029
1036
  });
1030
1037
  }
1038
+
1039
+ if (scopedLogger) {
1040
+ scopedLogger[level](text.trim(), { external: true });
1041
+ }
1031
1042
  }
1032
1043
  } catch {
1033
1044
  // Stream may be closed
@@ -1067,7 +1078,18 @@ class JobsImpl implements Jobs {
1067
1078
 
1068
1079
  // Create scoped logger for this job execution
1069
1080
  const scopedLogger = this.logger?.scoped("job", job.id);
1070
- const result = await handler(job.data, scopedLogger ? { logger: scopedLogger } : undefined);
1081
+ const emit = this.createJobEmitter(job);
1082
+ const log = scopedLogger
1083
+ ? (level: LogLevel, message: string, data?: Record<string, any>) => {
1084
+ scopedLogger[level](message, data);
1085
+ }
1086
+ : undefined;
1087
+
1088
+ const result = await handler(job.data, {
1089
+ logger: scopedLogger,
1090
+ emit,
1091
+ log,
1092
+ });
1071
1093
 
1072
1094
  await this.adapter.update(job.id, {
1073
1095
  status: "completed",
@@ -1141,6 +1163,23 @@ class JobsImpl implements Jobs {
1141
1163
  this.activeJobs--;
1142
1164
  }
1143
1165
  }
1166
+
1167
+ private createJobEmitter(job: Job): JobHandlerContext["emit"] {
1168
+ const events = this.events;
1169
+ if (!events) return undefined;
1170
+ return async (event: string, data?: Record<string, any>) => {
1171
+ const payload = {
1172
+ jobId: job.id,
1173
+ name: job.name,
1174
+ event,
1175
+ data,
1176
+ };
1177
+
1178
+ await events.emit("job.event", payload);
1179
+ await events.emit(`job.${job.name}.event`, payload);
1180
+ await events.emit(`job.${job.id}.event`, payload);
1181
+ };
1182
+ }
1144
1183
  }
1145
1184
 
1146
1185
  export function createJobs(config?: JobsConfig): Jobs {
@@ -68,7 +68,7 @@ export async function bootstrapSubprocess(
68
68
  const logs = createLogs({ adapter: new MemoryLogsAdapter(), events });
69
69
  const logger = createLogger();
70
70
 
71
- const cron = createCron({ logger });
71
+ const cron = createCron({ logger, events });
72
72
 
73
73
  const jobAdapter = new KyselyJobAdapter(db, { cleanupDays: 0 });
74
74
  const workflowAdapter = new KyselyWorkflowAdapter(db, { cleanupDays: 0 });
@@ -3,6 +3,7 @@
3
3
  // Communicates through an event callback interface - no knowledge of IPC, SSE, or process management.
4
4
 
5
5
  import type { CoreServices } from "../core";
6
+ import type { LogLevel } from "./logger";
6
7
  import type { Jobs } from "./jobs";
7
8
  import type {
8
9
  WorkflowAdapter,
@@ -482,6 +483,35 @@ export class WorkflowStateMachine {
482
483
  const adapter = this.adapter;
483
484
  const instanceId = instance.id;
484
485
 
486
+ const scopedLogger = this.core?.logger?.scoped("workflow", instance.id);
487
+ const emit = this.core?.events
488
+ ? async (event: string, data?: Record<string, any>) => {
489
+ const payload = {
490
+ instanceId: instance.id,
491
+ workflowName: instance.workflowName,
492
+ event,
493
+ data,
494
+ };
495
+
496
+ await this.core!.events.emit("workflow.event", payload);
497
+ await this.core!.events.emit(`workflow.${instance.workflowName}.event`, payload);
498
+ await this.core!.events.emit(`workflow.${instance.id}.event`, payload);
499
+ }
500
+ : undefined;
501
+
502
+ const log = scopedLogger
503
+ ? (level: LogLevel, message: string, data?: Record<string, any>) => {
504
+ scopedLogger[level](message, data);
505
+ }
506
+ : undefined;
507
+
508
+ const core = this.core
509
+ ? {
510
+ ...this.core,
511
+ logger: scopedLogger ?? this.core.logger,
512
+ }
513
+ : this.core;
514
+
485
515
  return {
486
516
  input: instance.input,
487
517
  steps,
@@ -490,7 +520,10 @@ export class WorkflowStateMachine {
490
520
  getStepResult: <T = any>(stepName: string): T | undefined => {
491
521
  return steps[stepName] as T | undefined;
492
522
  },
493
- core: this.core!,
523
+ core: core!,
524
+ logger: scopedLogger,
525
+ emit,
526
+ log,
494
527
  plugins: this.plugins,
495
528
  metadata,
496
529
  setMetadata: async (key: string, value: any): Promise<void> => {
@@ -14,6 +14,7 @@ import type { SSE } from "./sse";
14
14
  import type { z } from "zod";
15
15
  import { sql } from "kysely";
16
16
  import type { CoreServices } from "../core";
17
+ import type { Logger, LogLevel } from "./logger";
17
18
  import { dirname, join, resolve } from "node:path";
18
19
  import { fileURLToPath, pathToFileURL } from "node:url";
19
20
  import {
@@ -243,6 +244,12 @@ export interface WorkflowContext {
243
244
  getStepResult<T = any>(stepName: string): T | undefined;
244
245
  /** Core services (logger, events, cache, etc.) */
245
246
  core: CoreServices;
247
+ /** Scoped logger for this workflow instance (source=workflow, sourceId=instanceId) */
248
+ logger?: Logger;
249
+ /** Emit a workflow-scoped custom event */
250
+ emit?: (event: string, data?: Record<string, any>) => Promise<void>;
251
+ /** Write a scoped log entry for this workflow instance */
252
+ log?: (level: LogLevel, message: string, data?: Record<string, any>) => void;
246
253
  /** Plugin services - available for business logic in workflow handlers */
247
254
  plugins: Record<string, any>;
248
255
  /**
package/src/server.ts CHANGED
@@ -264,6 +264,7 @@ export class AppServer {
264
264
  const cron = createCron({
265
265
  ...options.cron,
266
266
  logger,
267
+ events,
267
268
  });
268
269
 
269
270
  // Create adapters - use Kysely by default, or legacy SQLite if requested