@donkeylabs/server 2.0.23 → 2.0.25
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 +28 -2
- package/docs/jobs.md +15 -0
- package/docs/workflows.md +25 -0
- package/package.json +1 -1
- package/src/core/cron.ts +50 -7
- package/src/core/index.ts +2 -0
- package/src/core/jobs.ts +42 -3
- package/src/core/subprocess-bootstrap.ts +1 -1
- package/src/core/workflow-executor.ts +21 -0
- package/src/core/workflow-socket.ts +14 -1
- package/src/core/workflow-state-machine.ts +64 -1
- package/src/core/workflows.ts +54 -0
- package/src/server.ts +1 -0
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
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?:
|
|
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
|
|
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 });
|
|
@@ -91,6 +91,27 @@ async function main(): Promise<void> {
|
|
|
91
91
|
plugins: bootstrap.manager.getServices(),
|
|
92
92
|
events: createIpcEventBridge(socket, instanceId),
|
|
93
93
|
pollInterval: 1000,
|
|
94
|
+
emitCustomEvent: async (payload) => {
|
|
95
|
+
sendEvent(socket, {
|
|
96
|
+
type: "event",
|
|
97
|
+
instanceId: payload.instanceId,
|
|
98
|
+
workflowName: payload.workflowName,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
event: payload.event,
|
|
101
|
+
data: payload.data,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
emitLog: async (payload) => {
|
|
105
|
+
sendEvent(socket, {
|
|
106
|
+
type: "log",
|
|
107
|
+
instanceId: payload.instanceId,
|
|
108
|
+
workflowName: payload.workflowName,
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
level: payload.level,
|
|
111
|
+
message: payload.message,
|
|
112
|
+
data: payload.data,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
94
115
|
});
|
|
95
116
|
|
|
96
117
|
sendEvent(socket, {
|
|
@@ -11,6 +11,8 @@ import { createServer as createNetServer } from "node:net";
|
|
|
11
11
|
// Message Protocol Types
|
|
12
12
|
// ============================================
|
|
13
13
|
|
|
14
|
+
import type { LogLevel } from "./logger";
|
|
15
|
+
|
|
14
16
|
export type WorkflowEventType =
|
|
15
17
|
| "ready"
|
|
16
18
|
| "started"
|
|
@@ -20,11 +22,14 @@ export type WorkflowEventType =
|
|
|
20
22
|
| "step.failed"
|
|
21
23
|
| "progress"
|
|
22
24
|
| "completed"
|
|
23
|
-
| "failed"
|
|
25
|
+
| "failed"
|
|
26
|
+
| "event"
|
|
27
|
+
| "log";
|
|
24
28
|
|
|
25
29
|
export interface WorkflowEvent {
|
|
26
30
|
type: WorkflowEventType;
|
|
27
31
|
instanceId: string;
|
|
32
|
+
workflowName?: string;
|
|
28
33
|
timestamp: number;
|
|
29
34
|
stepName?: string;
|
|
30
35
|
/** Step type (for step.started events) */
|
|
@@ -36,6 +41,14 @@ export interface WorkflowEvent {
|
|
|
36
41
|
totalSteps?: number;
|
|
37
42
|
/** Next step to execute (for step.completed events) */
|
|
38
43
|
nextStep?: string;
|
|
44
|
+
/** Custom event name (for event type) */
|
|
45
|
+
event?: string;
|
|
46
|
+
/** Custom event payload or log data */
|
|
47
|
+
data?: Record<string, any>;
|
|
48
|
+
/** Log level (for log type) */
|
|
49
|
+
level?: LogLevel;
|
|
50
|
+
/** Log message (for log type) */
|
|
51
|
+
message?: string;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
export interface ProxyRequest {
|
|
@@ -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,
|
|
@@ -44,6 +45,19 @@ export interface StateMachineConfig {
|
|
|
44
45
|
jobs?: Jobs;
|
|
45
46
|
/** Poll interval for checking job completion (ms) */
|
|
46
47
|
pollInterval?: number;
|
|
48
|
+
emitCustomEvent?: (payload: {
|
|
49
|
+
instanceId: string;
|
|
50
|
+
workflowName: string;
|
|
51
|
+
event: string;
|
|
52
|
+
data?: Record<string, any>;
|
|
53
|
+
}) => Promise<void>;
|
|
54
|
+
emitLog?: (payload: {
|
|
55
|
+
instanceId: string;
|
|
56
|
+
workflowName: string;
|
|
57
|
+
level: LogLevel;
|
|
58
|
+
message: string;
|
|
59
|
+
data?: Record<string, any>;
|
|
60
|
+
}) => Promise<void>;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
// ============================================
|
|
@@ -57,6 +71,8 @@ export class WorkflowStateMachine {
|
|
|
57
71
|
private events: StateMachineEvents;
|
|
58
72
|
private jobs?: Jobs;
|
|
59
73
|
private pollInterval: number;
|
|
74
|
+
private emitCustomEvent?: StateMachineConfig["emitCustomEvent"];
|
|
75
|
+
private emitLog?: StateMachineConfig["emitLog"];
|
|
60
76
|
private cancelledInstances = new Set<string>();
|
|
61
77
|
|
|
62
78
|
constructor(config: StateMachineConfig) {
|
|
@@ -66,6 +82,8 @@ export class WorkflowStateMachine {
|
|
|
66
82
|
this.events = config.events;
|
|
67
83
|
this.jobs = config.jobs;
|
|
68
84
|
this.pollInterval = config.pollInterval ?? 1000;
|
|
85
|
+
this.emitCustomEvent = config.emitCustomEvent;
|
|
86
|
+
this.emitLog = config.emitLog;
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
/**
|
|
@@ -482,6 +500,48 @@ export class WorkflowStateMachine {
|
|
|
482
500
|
const adapter = this.adapter;
|
|
483
501
|
const instanceId = instance.id;
|
|
484
502
|
|
|
503
|
+
const scopedLogger = this.core?.logger?.scoped("workflow", instance.id);
|
|
504
|
+
const emit = async (event: string, data?: Record<string, any>) => {
|
|
505
|
+
const payload = {
|
|
506
|
+
instanceId: instance.id,
|
|
507
|
+
workflowName: instance.workflowName,
|
|
508
|
+
event,
|
|
509
|
+
data,
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
if (this.core?.events) {
|
|
513
|
+
await this.core.events.emit("workflow.event", payload);
|
|
514
|
+
await this.core.events.emit(`workflow.${instance.workflowName}.event`, payload);
|
|
515
|
+
await this.core.events.emit(`workflow.${instance.id}.event`, payload);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (this.emitCustomEvent) {
|
|
519
|
+
await this.emitCustomEvent(payload);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const log = (level: LogLevel, message: string, data?: Record<string, any>) => {
|
|
524
|
+
if (scopedLogger) {
|
|
525
|
+
scopedLogger[level](message, data);
|
|
526
|
+
}
|
|
527
|
+
if (this.emitLog) {
|
|
528
|
+
return this.emitLog({
|
|
529
|
+
instanceId: instance.id,
|
|
530
|
+
workflowName: instance.workflowName,
|
|
531
|
+
level,
|
|
532
|
+
message,
|
|
533
|
+
data,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const core = this.core
|
|
539
|
+
? {
|
|
540
|
+
...this.core,
|
|
541
|
+
logger: scopedLogger ?? this.core.logger,
|
|
542
|
+
}
|
|
543
|
+
: this.core;
|
|
544
|
+
|
|
485
545
|
return {
|
|
486
546
|
input: instance.input,
|
|
487
547
|
steps,
|
|
@@ -490,7 +550,10 @@ export class WorkflowStateMachine {
|
|
|
490
550
|
getStepResult: <T = any>(stepName: string): T | undefined => {
|
|
491
551
|
return steps[stepName] as T | undefined;
|
|
492
552
|
},
|
|
493
|
-
core:
|
|
553
|
+
core: core!,
|
|
554
|
+
logger: scopedLogger,
|
|
555
|
+
emit,
|
|
556
|
+
log,
|
|
494
557
|
plugins: this.plugins,
|
|
495
558
|
metadata,
|
|
496
559
|
setMetadata: async (key: string, value: any): Promise<void> => {
|
package/src/core/workflows.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -1468,6 +1475,53 @@ class WorkflowsImpl implements Workflows {
|
|
|
1468
1475
|
break;
|
|
1469
1476
|
}
|
|
1470
1477
|
|
|
1478
|
+
case "event": {
|
|
1479
|
+
const workflowName = event.workflowName ?? (await this.adapter.getInstance(instanceId))?.workflowName;
|
|
1480
|
+
const payload = {
|
|
1481
|
+
instanceId,
|
|
1482
|
+
workflowName,
|
|
1483
|
+
event: event.event,
|
|
1484
|
+
data: event.data,
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
await this.emitEvent("workflow.event", payload);
|
|
1488
|
+
if (workflowName) {
|
|
1489
|
+
await this.emitEvent(`workflow.${workflowName}.event`, payload);
|
|
1490
|
+
}
|
|
1491
|
+
await this.emitEvent(`workflow.${instanceId}.event`, payload);
|
|
1492
|
+
|
|
1493
|
+
if (this.sse) {
|
|
1494
|
+
this.sse.broadcast(`workflow:${instanceId}`, "event", payload);
|
|
1495
|
+
this.sse.broadcast("workflows:all", "workflow.event", payload);
|
|
1496
|
+
}
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
case "log": {
|
|
1501
|
+
const workflowName = event.workflowName ?? (await this.adapter.getInstance(instanceId))?.workflowName;
|
|
1502
|
+
|
|
1503
|
+
if (this.core?.logs && event.level && event.message) {
|
|
1504
|
+
this.core.logs.write({
|
|
1505
|
+
level: event.level,
|
|
1506
|
+
message: event.message,
|
|
1507
|
+
source: "workflow",
|
|
1508
|
+
sourceId: instanceId,
|
|
1509
|
+
data: event.data,
|
|
1510
|
+
context: workflowName ? { workflowName } : undefined,
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
if (this.sse) {
|
|
1515
|
+
this.sse.broadcast(`workflow:${instanceId}`, "log", {
|
|
1516
|
+
level: event.level,
|
|
1517
|
+
message: event.message,
|
|
1518
|
+
data: event.data,
|
|
1519
|
+
workflowName,
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1471
1525
|
case "completed": {
|
|
1472
1526
|
// Clean up isolated process tracking
|
|
1473
1527
|
this.cleanupIsolatedProcess(instanceId);
|