@donkeylabs/server 2.0.20 → 2.0.22
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/workflows.md +73 -7
- package/package.json +2 -2
- package/src/admin/dashboard.ts +74 -3
- package/src/admin/routes.ts +62 -0
- package/src/core/cron.ts +17 -10
- package/src/core/index.ts +28 -0
- package/src/core/jobs.ts +8 -2
- package/src/core/logger.ts +14 -0
- package/src/core/logs-adapter-kysely.ts +287 -0
- package/src/core/logs-transport.ts +83 -0
- package/src/core/logs.ts +398 -0
- package/src/core/workflow-executor.ts +116 -337
- package/src/core/workflow-socket.ts +2 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +399 -0
- package/src/core/workflows.ts +300 -909
- package/src/core.ts +2 -0
- package/src/harness.ts +4 -0
- package/src/index.ts +10 -0
- package/src/server.ts +44 -5
- /package/{CLAUDE.md → agents.md} +0 -0
package/docs/workflows.md
CHANGED
|
@@ -79,7 +79,7 @@ const orderWorkflow = workflow("process-order")
|
|
|
79
79
|
### 2. Register and Start
|
|
80
80
|
|
|
81
81
|
```typescript
|
|
82
|
-
// Register the workflow
|
|
82
|
+
// Register the workflow — modulePath is auto-detected from build()
|
|
83
83
|
ctx.core.workflows.register(orderWorkflow);
|
|
84
84
|
|
|
85
85
|
// Start an instance
|
|
@@ -462,6 +462,60 @@ for (const event of workflowEvents) {
|
|
|
462
462
|
}
|
|
463
463
|
```
|
|
464
464
|
|
|
465
|
+
## Execution Modes
|
|
466
|
+
|
|
467
|
+
Workflows support two execution modes controlled by the `.isolated()` builder method:
|
|
468
|
+
|
|
469
|
+
### Isolated Mode (Default)
|
|
470
|
+
|
|
471
|
+
By default, workflows run in a **separate subprocess**. This prevents long-running step handlers from blocking the main server's event loop. The subprocess owns its own database connection and state machine, communicating events back to the main server via Unix sockets (TCP on Windows).
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
// Isolated mode (default) - runs in subprocess
|
|
475
|
+
const myWorkflow = workflow("heavy-processing")
|
|
476
|
+
.task("compute", {
|
|
477
|
+
handler: async (input, ctx) => {
|
|
478
|
+
// CPU-intensive work runs in subprocess, won't block the server
|
|
479
|
+
return await heavyComputation(input);
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
.build();
|
|
483
|
+
|
|
484
|
+
// Just register — modulePath is auto-detected from build()
|
|
485
|
+
ctx.core.workflows.register(myWorkflow);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
> **Advanced:** The module path is captured automatically when you call `.build()`. If you re-export a workflow definition from a different module, pass `{ modulePath: import.meta.url }` explicitly so the subprocess can find the definition.
|
|
489
|
+
|
|
490
|
+
### Inline Mode
|
|
491
|
+
|
|
492
|
+
For lightweight workflows that complete quickly, you can opt into inline execution:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
const quickWorkflow = workflow("quick-validation")
|
|
496
|
+
.isolated(false) // Run in the main server process
|
|
497
|
+
.task("validate", {
|
|
498
|
+
handler: async (input, ctx) => {
|
|
499
|
+
return { valid: true };
|
|
500
|
+
},
|
|
501
|
+
end: true,
|
|
502
|
+
})
|
|
503
|
+
.build();
|
|
504
|
+
|
|
505
|
+
// No modulePath needed for inline workflows
|
|
506
|
+
ctx.core.workflows.register(quickWorkflow);
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Choosing a Mode
|
|
510
|
+
|
|
511
|
+
| | Isolated (default) | Inline |
|
|
512
|
+
|---|---|---|
|
|
513
|
+
| Step types | All (task, choice, parallel, pass) | All (task, choice, parallel, pass) |
|
|
514
|
+
| Event loop | Separate process, won't block server | Runs on main thread |
|
|
515
|
+
| Plugin access | Via IPC proxy | Direct access |
|
|
516
|
+
| Best for | Long-running, CPU-intensive workflows | Quick validations, lightweight flows |
|
|
517
|
+
| Setup | `workflows.register(wf)` | `workflows.register(wf)` |
|
|
518
|
+
|
|
465
519
|
## API Reference
|
|
466
520
|
|
|
467
521
|
### Workflows Service
|
|
@@ -469,7 +523,7 @@ for (const event of workflowEvents) {
|
|
|
469
523
|
```typescript
|
|
470
524
|
interface Workflows {
|
|
471
525
|
/** Register a workflow definition */
|
|
472
|
-
register(definition: WorkflowDefinition): void;
|
|
526
|
+
register(definition: WorkflowDefinition, options?: WorkflowRegisterOptions): void;
|
|
473
527
|
|
|
474
528
|
/** Start a new workflow instance */
|
|
475
529
|
start<T = any>(workflowName: string, input: T): Promise<string>;
|
|
@@ -486,9 +540,21 @@ interface Workflows {
|
|
|
486
540
|
/** Resume workflows after server restart */
|
|
487
541
|
resume(): Promise<void>;
|
|
488
542
|
|
|
543
|
+
/** Update metadata for a workflow instance */
|
|
544
|
+
updateMetadata(instanceId: string, metadata: Record<string, any>): Promise<void>;
|
|
545
|
+
|
|
489
546
|
/** Stop the workflow service */
|
|
490
547
|
stop(): Promise<void>;
|
|
491
548
|
}
|
|
549
|
+
|
|
550
|
+
interface WorkflowRegisterOptions {
|
|
551
|
+
/**
|
|
552
|
+
* Module path for isolated workflows.
|
|
553
|
+
* Auto-detected from build() in most cases.
|
|
554
|
+
* Only needed when re-exporting from a different module.
|
|
555
|
+
*/
|
|
556
|
+
modulePath?: string;
|
|
557
|
+
}
|
|
492
558
|
```
|
|
493
559
|
|
|
494
560
|
### Workflow Instance
|
|
@@ -576,13 +642,13 @@ const server = new AppServer({
|
|
|
576
642
|
Workflows automatically resume after server restart:
|
|
577
643
|
|
|
578
644
|
1. On startup, `workflows.resume()` is called
|
|
579
|
-
2. All instances with `status: "running"` are retrieved
|
|
580
|
-
3.
|
|
645
|
+
2. All instances with `status: "running"` are retrieved from the database
|
|
646
|
+
3. Isolated workflows re-launch a new subprocess that continues from the current step
|
|
647
|
+
4. Inline workflows re-create the state machine and continue from the current step
|
|
581
648
|
|
|
582
649
|
For this to work properly:
|
|
583
650
|
- Use a persistent adapter (not in-memory) in production
|
|
584
|
-
-
|
|
585
|
-
- The Jobs service must also support restart resilience
|
|
651
|
+
- Step handlers should be idempotent when possible (a step may re-execute after a crash)
|
|
586
652
|
|
|
587
653
|
## Complete Example
|
|
588
654
|
|
|
@@ -670,7 +736,7 @@ const onboardingWorkflow = workflow("user-onboarding")
|
|
|
670
736
|
// Setup server
|
|
671
737
|
const server = new AppServer({ db: createDatabase() });
|
|
672
738
|
|
|
673
|
-
// Register workflow
|
|
739
|
+
// Register workflow — modulePath auto-detected from build()
|
|
674
740
|
server.getCore().workflows.register(onboardingWorkflow);
|
|
675
741
|
|
|
676
742
|
// Start workflow from a route
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donkeylabs/server",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Type-safe plugin system for building RPC-style APIs with Bun",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"src",
|
|
44
44
|
"docs",
|
|
45
45
|
"examples",
|
|
46
|
-
"
|
|
46
|
+
"agents.md",
|
|
47
47
|
"context.d.ts",
|
|
48
48
|
"registry.d.ts",
|
|
49
49
|
"LICENSE",
|
package/src/admin/dashboard.ts
CHANGED
|
@@ -18,6 +18,7 @@ const icons = {
|
|
|
18
18
|
cache: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>`,
|
|
19
19
|
plugins: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/></svg>`,
|
|
20
20
|
routes: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>`,
|
|
21
|
+
logs: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg>`,
|
|
21
22
|
refresh: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>`,
|
|
22
23
|
server: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>`,
|
|
23
24
|
};
|
|
@@ -72,6 +73,7 @@ export function renderDashboardLayout(
|
|
|
72
73
|
{ id: "processes", label: "Processes", icon: icons.processes },
|
|
73
74
|
{ id: "workflows", label: "Workflows", icon: icons.workflows },
|
|
74
75
|
{ id: "audit", label: "Audit Logs", icon: icons.audit },
|
|
76
|
+
{ id: "logs", label: "Logs", icon: icons.logs },
|
|
75
77
|
{ id: "sse", label: "SSE Clients", icon: icons.sse },
|
|
76
78
|
{ id: "websocket", label: "WebSocket", icon: icons.websocket },
|
|
77
79
|
{ id: "events", label: "Events", icon: icons.events },
|
|
@@ -120,7 +122,7 @@ export function renderDashboardLayout(
|
|
|
120
122
|
<nav class="nav-section">
|
|
121
123
|
<div class="nav-section-title">Core Services</div>
|
|
122
124
|
${navItems
|
|
123
|
-
.slice(1,
|
|
125
|
+
.slice(1, 6)
|
|
124
126
|
.map(
|
|
125
127
|
(item) => `
|
|
126
128
|
<a href="/${prefix}.dashboard?view=${item.id}"
|
|
@@ -138,7 +140,7 @@ export function renderDashboardLayout(
|
|
|
138
140
|
<nav class="nav-section">
|
|
139
141
|
<div class="nav-section-title">Connections</div>
|
|
140
142
|
${navItems
|
|
141
|
-
.slice(
|
|
143
|
+
.slice(6, 9)
|
|
142
144
|
.map(
|
|
143
145
|
(item) => `
|
|
144
146
|
<a href="/${prefix}.dashboard?view=${item.id}"
|
|
@@ -156,7 +158,7 @@ export function renderDashboardLayout(
|
|
|
156
158
|
<nav class="nav-section">
|
|
157
159
|
<div class="nav-section-title">Configuration</div>
|
|
158
160
|
${navItems
|
|
159
|
-
.slice(
|
|
161
|
+
.slice(9)
|
|
160
162
|
.map(
|
|
161
163
|
(item) => `
|
|
162
164
|
<a href="/${prefix}.dashboard?view=${item.id}"
|
|
@@ -675,6 +677,75 @@ export function renderPlugins(prefix: string, plugins: any[]): string {
|
|
|
675
677
|
`;
|
|
676
678
|
}
|
|
677
679
|
|
|
680
|
+
export function renderLogs(prefix: string, logs: any[]): string {
|
|
681
|
+
const levelBadgeClass = (level: string) => {
|
|
682
|
+
switch (level) {
|
|
683
|
+
case "error": return "badge-failed";
|
|
684
|
+
case "warn": return "badge-pending";
|
|
685
|
+
case "info": return "badge-running";
|
|
686
|
+
case "debug": return "badge-completed";
|
|
687
|
+
default: return "";
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
return `
|
|
692
|
+
<div class="page-header">
|
|
693
|
+
<h2 class="page-title">Logs</h2>
|
|
694
|
+
<button class="btn" hx-get="/${prefix}.dashboard?view=logs&partial=1" hx-target="#main-content">
|
|
695
|
+
${icons.refresh}
|
|
696
|
+
Refresh
|
|
697
|
+
</button>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<div class="filters">
|
|
701
|
+
<select class="filter-select" hx-get="/${prefix}.dashboard?view=logs&partial=1" hx-target="#main-content" hx-include="this" name="status">
|
|
702
|
+
<option value="">All Sources</option>
|
|
703
|
+
<option value="system">System</option>
|
|
704
|
+
<option value="cron">Cron</option>
|
|
705
|
+
<option value="job">Job</option>
|
|
706
|
+
<option value="workflow">Workflow</option>
|
|
707
|
+
<option value="plugin">Plugin</option>
|
|
708
|
+
<option value="route">Route</option>
|
|
709
|
+
</select>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
<div class="card">
|
|
713
|
+
<div class="table-container">
|
|
714
|
+
<table>
|
|
715
|
+
<thead>
|
|
716
|
+
<tr>
|
|
717
|
+
<th>Level</th>
|
|
718
|
+
<th>Source</th>
|
|
719
|
+
<th>Source ID</th>
|
|
720
|
+
<th>Message</th>
|
|
721
|
+
<th>Timestamp</th>
|
|
722
|
+
</tr>
|
|
723
|
+
</thead>
|
|
724
|
+
<tbody>
|
|
725
|
+
${
|
|
726
|
+
logs.length === 0
|
|
727
|
+
? '<tr><td colspan="5" class="empty-state">No log entries found</td></tr>'
|
|
728
|
+
: logs
|
|
729
|
+
.map(
|
|
730
|
+
(log: any) => `
|
|
731
|
+
<tr>
|
|
732
|
+
<td><span class="badge ${levelBadgeClass(log.level)}">${log.level}</span></td>
|
|
733
|
+
<td>${log.source}</td>
|
|
734
|
+
<td class="mono truncate" title="${log.sourceId ?? ""}">${log.sourceId ?? "-"}</td>
|
|
735
|
+
<td class="truncate" title="${log.message}">${log.message.slice(0, 80)}${log.message.length > 80 ? "..." : ""}</td>
|
|
736
|
+
<td class="relative-time">${formatRelativeTime(log.timestamp)}</td>
|
|
737
|
+
</tr>
|
|
738
|
+
`
|
|
739
|
+
)
|
|
740
|
+
.join("")
|
|
741
|
+
}
|
|
742
|
+
</tbody>
|
|
743
|
+
</table>
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
`;
|
|
747
|
+
}
|
|
748
|
+
|
|
678
749
|
export function renderRoutes(prefix: string, routes: any[]): string {
|
|
679
750
|
return `
|
|
680
751
|
<div class="page-header">
|
package/src/admin/routes.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
renderProcessesList,
|
|
14
14
|
renderWorkflowsList,
|
|
15
15
|
renderAuditLogs,
|
|
16
|
+
renderLogs,
|
|
16
17
|
renderSSEClients,
|
|
17
18
|
renderWebSocketClients,
|
|
18
19
|
renderEvents,
|
|
@@ -124,6 +125,14 @@ export function createAdminRouter(config: AdminRouteContext) {
|
|
|
124
125
|
content = renderWorkflowsList(prefix, workflows);
|
|
125
126
|
break;
|
|
126
127
|
}
|
|
128
|
+
case "logs": {
|
|
129
|
+
const logEntries = await ctx.core.logs.query({
|
|
130
|
+
source: (status as any) || undefined,
|
|
131
|
+
limit: 100,
|
|
132
|
+
});
|
|
133
|
+
content = renderLogs(prefix, logEntries);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
127
136
|
case "audit": {
|
|
128
137
|
const logs = await ctx.core.audit.query({
|
|
129
138
|
limit: 100,
|
|
@@ -550,6 +559,59 @@ export function createAdminRouter(config: AdminRouteContext) {
|
|
|
550
559
|
},
|
|
551
560
|
});
|
|
552
561
|
|
|
562
|
+
// Logs list route
|
|
563
|
+
router.route("logs.list").typed(
|
|
564
|
+
defineRoute({
|
|
565
|
+
input: z.object({
|
|
566
|
+
source: z.enum(["system", "cron", "job", "workflow", "plugin", "route"]).optional(),
|
|
567
|
+
sourceId: z.string().optional(),
|
|
568
|
+
level: z.enum(["debug", "info", "warn", "error"]).optional(),
|
|
569
|
+
search: z.string().optional(),
|
|
570
|
+
startDate: z.string().optional(),
|
|
571
|
+
endDate: z.string().optional(),
|
|
572
|
+
limit: z.number().default(100),
|
|
573
|
+
offset: z.number().default(0),
|
|
574
|
+
}),
|
|
575
|
+
output: z.array(
|
|
576
|
+
z.object({
|
|
577
|
+
id: z.string(),
|
|
578
|
+
timestamp: z.string(),
|
|
579
|
+
level: z.string(),
|
|
580
|
+
message: z.string(),
|
|
581
|
+
source: z.string(),
|
|
582
|
+
sourceId: z.string().nullable(),
|
|
583
|
+
tags: z.array(z.string()).nullable(),
|
|
584
|
+
data: z.any().nullable(),
|
|
585
|
+
})
|
|
586
|
+
),
|
|
587
|
+
handle: async (input, ctx) => {
|
|
588
|
+
if (!checkAuth(ctx)) {
|
|
589
|
+
throw ctx.errors.Forbidden("Unauthorized");
|
|
590
|
+
}
|
|
591
|
+
const entries = await ctx.core.logs.query({
|
|
592
|
+
source: input.source as any,
|
|
593
|
+
sourceId: input.sourceId,
|
|
594
|
+
level: input.level as any,
|
|
595
|
+
search: input.search,
|
|
596
|
+
startDate: input.startDate ? new Date(input.startDate) : undefined,
|
|
597
|
+
endDate: input.endDate ? new Date(input.endDate) : undefined,
|
|
598
|
+
limit: input.limit,
|
|
599
|
+
offset: input.offset,
|
|
600
|
+
});
|
|
601
|
+
return entries.map((entry) => ({
|
|
602
|
+
id: entry.id,
|
|
603
|
+
timestamp: entry.timestamp.toISOString(),
|
|
604
|
+
level: entry.level,
|
|
605
|
+
message: entry.message,
|
|
606
|
+
source: entry.source,
|
|
607
|
+
sourceId: entry.sourceId ?? null,
|
|
608
|
+
tags: entry.tags ?? null,
|
|
609
|
+
data: entry.data ?? null,
|
|
610
|
+
}));
|
|
611
|
+
},
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
|
|
553
615
|
// Audit list route
|
|
554
616
|
router.route("audit.list").typed(
|
|
555
617
|
defineRoute({
|
package/src/core/cron.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Core Cron Service
|
|
2
2
|
// Schedule recurring tasks with cron expressions
|
|
3
3
|
|
|
4
|
+
import type { Logger } from "./logger";
|
|
5
|
+
|
|
4
6
|
export interface CronTask {
|
|
5
7
|
id: string;
|
|
6
8
|
name: string;
|
|
7
9
|
expression: string;
|
|
8
|
-
handler: () => void | Promise<void>;
|
|
10
|
+
handler: (logger?: Logger) => void | Promise<void>;
|
|
9
11
|
enabled: boolean;
|
|
10
12
|
lastRun?: Date;
|
|
11
13
|
nextRun?: Date;
|
|
@@ -13,12 +15,13 @@ export interface CronTask {
|
|
|
13
15
|
|
|
14
16
|
export interface CronConfig {
|
|
15
17
|
timezone?: string; // For future use
|
|
18
|
+
logger?: Logger;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface Cron {
|
|
19
22
|
schedule(
|
|
20
23
|
expression: string,
|
|
21
|
-
handler: () => void | Promise<void>,
|
|
24
|
+
handler: (logger?: Logger) => void | Promise<void>,
|
|
22
25
|
options?: { name?: string; enabled?: boolean }
|
|
23
26
|
): string;
|
|
24
27
|
unschedule(taskId: string): boolean;
|
|
@@ -259,14 +262,15 @@ class CronImpl implements Cron {
|
|
|
259
262
|
private running = false;
|
|
260
263
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
261
264
|
private taskCounter = 0;
|
|
265
|
+
private logger?: Logger;
|
|
262
266
|
|
|
263
|
-
constructor(
|
|
264
|
-
|
|
267
|
+
constructor(config: CronConfig = {}) {
|
|
268
|
+
this.logger = config.logger;
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
schedule(
|
|
268
272
|
expression: string,
|
|
269
|
-
handler: () => void | Promise<void>,
|
|
273
|
+
handler: (logger?: Logger) => void | Promise<void>,
|
|
270
274
|
options: { name?: string; enabled?: boolean } = {}
|
|
271
275
|
): string {
|
|
272
276
|
const id = `cron_${++this.taskCounter}_${Date.now()}`;
|
|
@@ -336,7 +340,8 @@ class CronImpl implements Cron {
|
|
|
336
340
|
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
337
341
|
|
|
338
342
|
task.lastRun = new Date();
|
|
339
|
-
|
|
343
|
+
const scopedLogger = this.logger?.scoped("cron", task.name);
|
|
344
|
+
await task.handler(scopedLogger);
|
|
340
345
|
}
|
|
341
346
|
|
|
342
347
|
start(): void {
|
|
@@ -358,8 +363,9 @@ class CronImpl implements Cron {
|
|
|
358
363
|
task.lastRun = now;
|
|
359
364
|
task.nextRun = cronExpr.getNextRun(now);
|
|
360
365
|
|
|
361
|
-
// Execute handler (fire and forget, but log errors)
|
|
362
|
-
|
|
366
|
+
// Execute handler with scoped logger (fire and forget, but log errors)
|
|
367
|
+
const scopedLogger = this.logger?.scoped("cron", task.name);
|
|
368
|
+
Promise.resolve(task.handler(scopedLogger)).catch(err => {
|
|
363
369
|
console.error(`[Cron] Task "${task.name}" failed:`, err);
|
|
364
370
|
});
|
|
365
371
|
}
|
|
@@ -388,8 +394,9 @@ class CronImpl implements Cron {
|
|
|
388
394
|
while (missedRun && missedRun < now && catchUpCount < maxCatchUp) {
|
|
389
395
|
console.log(`[Cron] Catching up missed run for "${task.name}" at ${missedRun.toISOString()}`);
|
|
390
396
|
|
|
391
|
-
// Execute the handler asynchronously
|
|
392
|
-
|
|
397
|
+
// Execute the handler asynchronously with scoped logger
|
|
398
|
+
const scopedLogger = this.logger?.scoped("cron", task.name);
|
|
399
|
+
Promise.resolve(task.handler(scopedLogger)).catch(err => {
|
|
393
400
|
console.error(`[Cron] Catch-up task "${task.name}" failed:`, err);
|
|
394
401
|
});
|
|
395
402
|
|
package/src/core/index.ts
CHANGED
|
@@ -217,6 +217,12 @@ export {
|
|
|
217
217
|
createProcessSocketServer,
|
|
218
218
|
} from "./process-socket";
|
|
219
219
|
|
|
220
|
+
export {
|
|
221
|
+
WorkflowStateMachine,
|
|
222
|
+
type StateMachineEvents,
|
|
223
|
+
type StateMachineConfig,
|
|
224
|
+
} from "./workflow-state-machine";
|
|
225
|
+
|
|
220
226
|
export {
|
|
221
227
|
KyselyWorkflowAdapter,
|
|
222
228
|
type KyselyWorkflowAdapterConfig,
|
|
@@ -266,3 +272,25 @@ export {
|
|
|
266
272
|
|
|
267
273
|
export { LocalStorageAdapter } from "./storage-adapter-local";
|
|
268
274
|
export { S3StorageAdapter } from "./storage-adapter-s3";
|
|
275
|
+
|
|
276
|
+
export {
|
|
277
|
+
type Logs,
|
|
278
|
+
type LogSource,
|
|
279
|
+
type PersistentLogEntry,
|
|
280
|
+
type LogsQueryFilters,
|
|
281
|
+
type LogsRetentionConfig,
|
|
282
|
+
type LogsConfig,
|
|
283
|
+
type LogsAdapter,
|
|
284
|
+
MemoryLogsAdapter,
|
|
285
|
+
createLogs,
|
|
286
|
+
} from "./logs";
|
|
287
|
+
|
|
288
|
+
export {
|
|
289
|
+
KyselyLogsAdapter,
|
|
290
|
+
type KyselyLogsAdapterConfig,
|
|
291
|
+
} from "./logs-adapter-kysely";
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
PersistentTransport,
|
|
295
|
+
type PersistentTransportConfig,
|
|
296
|
+
} from "./logs-transport";
|
package/src/core/jobs.ts
CHANGED
|
@@ -3,6 +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
7
|
import type {
|
|
7
8
|
ExternalJobConfig,
|
|
8
9
|
ExternalJob,
|
|
@@ -58,7 +59,7 @@ export interface Job {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export interface JobHandler<T = any, R = any> {
|
|
61
|
-
(data: T): Promise<R>;
|
|
62
|
+
(data: T, ctx?: { logger: Logger }): Promise<R>;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/** Options for listing all jobs */
|
|
@@ -94,6 +95,7 @@ export interface JobAdapter {
|
|
|
94
95
|
export interface JobsConfig {
|
|
95
96
|
adapter?: JobAdapter;
|
|
96
97
|
events?: Events;
|
|
98
|
+
logger?: Logger;
|
|
97
99
|
concurrency?: number; // Max concurrent jobs, default 5
|
|
98
100
|
pollInterval?: number; // ms, default 1000
|
|
99
101
|
maxAttempts?: number; // Default retry attempts, default 3
|
|
@@ -247,6 +249,7 @@ class JobsImpl implements Jobs {
|
|
|
247
249
|
private adapter: JobAdapter;
|
|
248
250
|
private sqliteAdapter?: SqliteJobAdapter;
|
|
249
251
|
private events?: Events;
|
|
252
|
+
private logger?: Logger;
|
|
250
253
|
private handlers = new Map<string, JobHandler>();
|
|
251
254
|
private running = false;
|
|
252
255
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -268,6 +271,7 @@ class JobsImpl implements Jobs {
|
|
|
268
271
|
|
|
269
272
|
constructor(config: JobsConfig = {}) {
|
|
270
273
|
this.events = config.events;
|
|
274
|
+
this.logger = config.logger;
|
|
271
275
|
this.concurrency = config.concurrency ?? 5;
|
|
272
276
|
this.pollInterval = config.pollInterval ?? 1000;
|
|
273
277
|
this.defaultMaxAttempts = config.maxAttempts ?? 3;
|
|
@@ -1061,7 +1065,9 @@ class JobsImpl implements Jobs {
|
|
|
1061
1065
|
attempts: job.attempts + 1,
|
|
1062
1066
|
});
|
|
1063
1067
|
|
|
1064
|
-
|
|
1068
|
+
// Create scoped logger for this job execution
|
|
1069
|
+
const scopedLogger = this.logger?.scoped("job", job.id);
|
|
1070
|
+
const result = await handler(job.data, scopedLogger ? { logger: scopedLogger } : undefined);
|
|
1065
1071
|
|
|
1066
1072
|
await this.adapter.update(job.id, {
|
|
1067
1073
|
status: "completed",
|
package/src/core/logger.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface Logger {
|
|
|
32
32
|
child(context: Record<string, any>): Logger;
|
|
33
33
|
/** Create a tagged child logger with colored prefix */
|
|
34
34
|
tag(name: string): Logger;
|
|
35
|
+
/** Create a scoped child logger with source attribution for persistent logging */
|
|
36
|
+
scoped(source: string, sourceId: string): Logger;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
@@ -55,6 +57,14 @@ const TAG_COLORS: ((s: string) => string)[] = [
|
|
|
55
57
|
const tagColorCache = new Map<string, (s: string) => string>();
|
|
56
58
|
let colorIndex = 0;
|
|
57
59
|
|
|
60
|
+
// Pre-seed fixed colors for source types
|
|
61
|
+
tagColorCache.set("cron", pc.yellow);
|
|
62
|
+
tagColorCache.set("job", pc.magenta);
|
|
63
|
+
tagColorCache.set("workflow", pc.cyan);
|
|
64
|
+
tagColorCache.set("plugin", pc.green);
|
|
65
|
+
tagColorCache.set("system", pc.blue);
|
|
66
|
+
tagColorCache.set("route", pc.red);
|
|
67
|
+
|
|
58
68
|
/**
|
|
59
69
|
* Get a consistent color for a tag name.
|
|
60
70
|
* Same tag always gets the same color within a process.
|
|
@@ -187,6 +197,10 @@ class LoggerImpl implements Logger {
|
|
|
187
197
|
[...this.tags, name]
|
|
188
198
|
);
|
|
189
199
|
}
|
|
200
|
+
|
|
201
|
+
scoped(source: string, sourceId: string): Logger {
|
|
202
|
+
return this.tag(source).child({ logSource: source, logSourceId: sourceId });
|
|
203
|
+
}
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
export function createLogger(config?: LoggerConfig): Logger {
|