@donkeylabs/server 2.0.19 → 2.0.21
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/caching-strategies.md +677 -0
- package/docs/dev-experience.md +656 -0
- package/docs/hot-reload-limitations.md +166 -0
- package/docs/load-testing.md +974 -0
- package/docs/plugin-registry-design.md +1064 -0
- package/docs/production.md +1229 -0
- package/docs/workflows.md +90 -3
- package/package.json +1 -1
- package/src/admin/routes.ts +153 -0
- package/src/core/cron.ts +90 -9
- package/src/core/index.ts +31 -0
- package/src/core/job-adapter-kysely.ts +176 -73
- package/src/core/job-adapter-sqlite.ts +10 -0
- package/src/core/jobs.ts +112 -17
- package/src/core/migrations/workflows/002_add_metadata_column.ts +28 -0
- package/src/core/process-adapter-kysely.ts +62 -21
- package/src/core/storage-adapter-local.test.ts +199 -0
- package/src/core/storage.test.ts +197 -0
- package/src/core/workflow-adapter-kysely.ts +66 -19
- package/src/core/workflow-executor.ts +239 -0
- package/src/core/workflow-proxy.ts +238 -0
- package/src/core/workflow-socket.ts +449 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +758 -0
- package/src/core/workflows.ts +705 -595
- package/src/core.ts +17 -6
- package/src/index.ts +14 -0
- package/src/testing/database.test.ts +263 -0
- package/src/testing/database.ts +173 -0
- package/src/testing/e2e.test.ts +189 -0
- package/src/testing/e2e.ts +272 -0
- package/src/testing/index.ts +18 -0
package/docs/workflows.md
CHANGED
|
@@ -305,6 +305,21 @@ interface WorkflowContext {
|
|
|
305
305
|
|
|
306
306
|
/** Type-safe step result getter */
|
|
307
307
|
getStepResult<T>(stepName: string): T | undefined;
|
|
308
|
+
|
|
309
|
+
/** Core services (logger, events, cache, jobs, sse, etc.) */
|
|
310
|
+
core: CoreServices;
|
|
311
|
+
|
|
312
|
+
/** Plugin services - access your plugins' service methods */
|
|
313
|
+
plugins: Record<string, any>;
|
|
314
|
+
|
|
315
|
+
/** Custom metadata that persists across steps (read-only snapshot) */
|
|
316
|
+
metadata: Record<string, any>;
|
|
317
|
+
|
|
318
|
+
/** Set a metadata value that persists across workflow steps */
|
|
319
|
+
setMetadata(key: string, value: any): Promise<void>;
|
|
320
|
+
|
|
321
|
+
/** Get a typed metadata value */
|
|
322
|
+
getMetadata<T>(key: string): T | undefined;
|
|
308
323
|
}
|
|
309
324
|
```
|
|
310
325
|
|
|
@@ -320,11 +335,59 @@ Example usage in step configuration:
|
|
|
320
335
|
handler: async (input, ctx) => {
|
|
321
336
|
// Access any step's output
|
|
322
337
|
const calcResult = ctx.getStepResult<{ amount: number }>("calculate");
|
|
338
|
+
|
|
339
|
+
// Access plugin services
|
|
340
|
+
const order = await ctx.plugins.orders.getById(input.orderId);
|
|
341
|
+
|
|
342
|
+
// Use core services
|
|
343
|
+
ctx.core.logger.info("Processing order", { orderId: input.orderId });
|
|
344
|
+
|
|
323
345
|
return { processed: true, amount: calcResult?.amount };
|
|
324
346
|
},
|
|
325
347
|
})
|
|
326
348
|
```
|
|
327
349
|
|
|
350
|
+
### Cross-Step Metadata
|
|
351
|
+
|
|
352
|
+
Use metadata to share data across workflow steps that isn't part of the normal step output flow:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
.task("validate", {
|
|
356
|
+
inputSchema: z.object({ orderId: z.string() }),
|
|
357
|
+
handler: async (input, ctx) => {
|
|
358
|
+
// Store correlation ID for logging/tracing across steps
|
|
359
|
+
await ctx.setMetadata("correlationId", crypto.randomUUID());
|
|
360
|
+
|
|
361
|
+
// Store complex context that multiple steps need
|
|
362
|
+
await ctx.setMetadata("orderContext", {
|
|
363
|
+
customer: await ctx.plugins.customers.getByOrder(input.orderId),
|
|
364
|
+
flags: { expedited: false, giftWrap: false },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return { valid: true };
|
|
368
|
+
},
|
|
369
|
+
})
|
|
370
|
+
.task("fulfill", {
|
|
371
|
+
handler: async (input, ctx) => {
|
|
372
|
+
// Read metadata from previous steps
|
|
373
|
+
const correlationId = ctx.getMetadata<string>("correlationId");
|
|
374
|
+
const orderCtx = ctx.getMetadata<{ customer: Customer; flags: object }>("orderContext");
|
|
375
|
+
|
|
376
|
+
ctx.core.logger.info("Fulfilling order", { correlationId, customer: orderCtx?.customer.id });
|
|
377
|
+
|
|
378
|
+
// Update metadata for downstream steps
|
|
379
|
+
await ctx.setMetadata("orderContext", {
|
|
380
|
+
...orderCtx,
|
|
381
|
+
flags: { ...orderCtx?.flags, fulfilled: true },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return { shipped: true };
|
|
385
|
+
},
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Metadata is persisted to the database and survives server restarts.
|
|
390
|
+
|
|
328
391
|
## Retry Configuration
|
|
329
392
|
|
|
330
393
|
Configure retries at the step level or set defaults for the entire workflow:
|
|
@@ -440,6 +503,8 @@ interface WorkflowInstance {
|
|
|
440
503
|
output?: any;
|
|
441
504
|
error?: string;
|
|
442
505
|
stepResults: Record<string, StepResult>;
|
|
506
|
+
/** Custom metadata that persists across steps */
|
|
507
|
+
metadata?: Record<string, any>;
|
|
443
508
|
createdAt: Date;
|
|
444
509
|
startedAt?: Date;
|
|
445
510
|
completedAt?: Date;
|
|
@@ -459,7 +524,28 @@ interface StepResult {
|
|
|
459
524
|
|
|
460
525
|
## Persistence
|
|
461
526
|
|
|
462
|
-
By default, workflows use
|
|
527
|
+
By default, workflows use a **Kysely database adapter** that stores workflow instances in the same database as your application. This provides automatic persistence and restart resilience.
|
|
528
|
+
|
|
529
|
+
The framework automatically creates and manages the `__donkeylabs_workflow_instances__` table with proper migrations.
|
|
530
|
+
|
|
531
|
+
### Built-in Kysely Adapter
|
|
532
|
+
|
|
533
|
+
The `KyselyWorkflowAdapter` is automatically configured when you provide a database connection:
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
const server = new AppServer({
|
|
537
|
+
db: createDatabase(), // Workflows will automatically persist to this database
|
|
538
|
+
workflows: {
|
|
539
|
+
pollInterval: 1000, // How often to check job completion
|
|
540
|
+
cleanupDays: 30, // Auto-cleanup completed workflows older than 30 days (0 to disable)
|
|
541
|
+
cleanupInterval: 3600000, // Cleanup check interval (default: 1 hour)
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Custom Adapter
|
|
547
|
+
|
|
548
|
+
For custom storage backends, implement the `WorkflowAdapter` interface:
|
|
463
549
|
|
|
464
550
|
```typescript
|
|
465
551
|
interface WorkflowAdapter {
|
|
@@ -469,6 +555,7 @@ interface WorkflowAdapter {
|
|
|
469
555
|
deleteInstance(instanceId: string): Promise<boolean>;
|
|
470
556
|
getInstancesByWorkflow(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
|
|
471
557
|
getRunningInstances(): Promise<WorkflowInstance[]>;
|
|
558
|
+
getAllInstances(options?: GetAllWorkflowsOptions): Promise<WorkflowInstance[]>;
|
|
472
559
|
}
|
|
473
560
|
```
|
|
474
561
|
|
|
@@ -478,8 +565,8 @@ Configure via `ServerConfig`:
|
|
|
478
565
|
const server = new AppServer({
|
|
479
566
|
db: createDatabase(),
|
|
480
567
|
workflows: {
|
|
481
|
-
adapter: new
|
|
482
|
-
pollInterval: 1000,
|
|
568
|
+
adapter: new MyCustomWorkflowAdapter(),
|
|
569
|
+
pollInterval: 1000,
|
|
483
570
|
},
|
|
484
571
|
});
|
|
485
572
|
```
|
package/package.json
CHANGED
package/src/admin/routes.ts
CHANGED
|
@@ -397,6 +397,159 @@ export function createAdminRouter(config: AdminRouteContext) {
|
|
|
397
397
|
})
|
|
398
398
|
);
|
|
399
399
|
|
|
400
|
+
// Workflows get route - full instance details with step results
|
|
401
|
+
router.route("workflows.get").typed(
|
|
402
|
+
defineRoute({
|
|
403
|
+
input: z.object({ instanceId: z.string() }),
|
|
404
|
+
output: z.object({
|
|
405
|
+
id: z.string(),
|
|
406
|
+
workflowName: z.string(),
|
|
407
|
+
status: z.string(),
|
|
408
|
+
currentStep: z.string().nullable(),
|
|
409
|
+
input: z.any(),
|
|
410
|
+
output: z.any().nullable(),
|
|
411
|
+
error: z.string().nullable(),
|
|
412
|
+
stepResults: z.record(
|
|
413
|
+
z.object({
|
|
414
|
+
stepName: z.string(),
|
|
415
|
+
status: z.string(),
|
|
416
|
+
input: z.any().optional(),
|
|
417
|
+
output: z.any().optional(),
|
|
418
|
+
error: z.string().optional(),
|
|
419
|
+
startedAt: z.string().nullable(),
|
|
420
|
+
completedAt: z.string().nullable(),
|
|
421
|
+
attempts: z.number(),
|
|
422
|
+
})
|
|
423
|
+
),
|
|
424
|
+
metadata: z.record(z.any()).nullable(),
|
|
425
|
+
createdAt: z.string(),
|
|
426
|
+
startedAt: z.string().nullable(),
|
|
427
|
+
completedAt: z.string().nullable(),
|
|
428
|
+
parentId: z.string().nullable(),
|
|
429
|
+
branchName: z.string().nullable(),
|
|
430
|
+
}).nullable(),
|
|
431
|
+
handle: async (input, ctx) => {
|
|
432
|
+
if (!checkAuth(ctx)) {
|
|
433
|
+
throw ctx.errors.Forbidden("Unauthorized");
|
|
434
|
+
}
|
|
435
|
+
const instance = await ctx.core.workflows.getInstance(input.instanceId);
|
|
436
|
+
if (!instance) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Transform step results with proper date serialization
|
|
441
|
+
const stepResults: Record<string, any> = {};
|
|
442
|
+
for (const [key, result] of Object.entries(instance.stepResults)) {
|
|
443
|
+
stepResults[key] = {
|
|
444
|
+
stepName: result.stepName,
|
|
445
|
+
status: result.status,
|
|
446
|
+
input: result.input,
|
|
447
|
+
output: result.output,
|
|
448
|
+
error: result.error,
|
|
449
|
+
startedAt: result.startedAt?.toISOString() ?? null,
|
|
450
|
+
completedAt: result.completedAt?.toISOString() ?? null,
|
|
451
|
+
attempts: result.attempts,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
id: instance.id,
|
|
457
|
+
workflowName: instance.workflowName,
|
|
458
|
+
status: instance.status,
|
|
459
|
+
currentStep: instance.currentStep ?? null,
|
|
460
|
+
input: instance.input,
|
|
461
|
+
output: instance.output ?? null,
|
|
462
|
+
error: instance.error ?? null,
|
|
463
|
+
stepResults,
|
|
464
|
+
metadata: instance.metadata ?? null,
|
|
465
|
+
createdAt: instance.createdAt.toISOString(),
|
|
466
|
+
startedAt: instance.startedAt?.toISOString() ?? null,
|
|
467
|
+
completedAt: instance.completedAt?.toISOString() ?? null,
|
|
468
|
+
parentId: instance.parentId ?? null,
|
|
469
|
+
branchName: instance.branchName ?? null,
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
})
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Workflows subscribe route - SSE for real-time workflow events
|
|
476
|
+
// Subscribes to workflow:{instanceId} channel for progress, completed, and failed events
|
|
477
|
+
router.route("workflows.subscribe").sse({
|
|
478
|
+
input: z.object({
|
|
479
|
+
instanceId: z.string(),
|
|
480
|
+
}),
|
|
481
|
+
events: {
|
|
482
|
+
progress: z.object({
|
|
483
|
+
progress: z.number(),
|
|
484
|
+
currentStep: z.string().optional(),
|
|
485
|
+
completedSteps: z.number(),
|
|
486
|
+
totalSteps: z.number(),
|
|
487
|
+
}),
|
|
488
|
+
"step.started": z.object({
|
|
489
|
+
stepName: z.string(),
|
|
490
|
+
}),
|
|
491
|
+
"step.completed": z.object({
|
|
492
|
+
stepName: z.string(),
|
|
493
|
+
output: z.any().optional(),
|
|
494
|
+
}),
|
|
495
|
+
"step.failed": z.object({
|
|
496
|
+
stepName: z.string(),
|
|
497
|
+
error: z.string(),
|
|
498
|
+
}),
|
|
499
|
+
completed: z.object({
|
|
500
|
+
output: z.any().optional(),
|
|
501
|
+
}),
|
|
502
|
+
failed: z.object({
|
|
503
|
+
error: z.string(),
|
|
504
|
+
}),
|
|
505
|
+
},
|
|
506
|
+
handle: (input, ctx) => {
|
|
507
|
+
if (!checkAuth(ctx)) {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
// Subscribe to the workflow-specific channel
|
|
511
|
+
return [`workflow:${input.instanceId}`];
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Workflows subscribe all route - SSE for all workflow events (useful for dashboard)
|
|
516
|
+
// Subscribes to workflow:* pattern for all workflow updates
|
|
517
|
+
router.route("workflows.subscribeAll").sse({
|
|
518
|
+
input: z.object({
|
|
519
|
+
// Optional filter by workflow name
|
|
520
|
+
workflowName: z.string().optional(),
|
|
521
|
+
}),
|
|
522
|
+
events: {
|
|
523
|
+
"workflow.started": z.object({
|
|
524
|
+
instanceId: z.string(),
|
|
525
|
+
workflowName: z.string(),
|
|
526
|
+
}),
|
|
527
|
+
"workflow.progress": z.object({
|
|
528
|
+
instanceId: z.string(),
|
|
529
|
+
workflowName: z.string(),
|
|
530
|
+
progress: z.number(),
|
|
531
|
+
currentStep: z.string().optional(),
|
|
532
|
+
}),
|
|
533
|
+
"workflow.completed": z.object({
|
|
534
|
+
instanceId: z.string(),
|
|
535
|
+
workflowName: z.string(),
|
|
536
|
+
}),
|
|
537
|
+
"workflow.failed": z.object({
|
|
538
|
+
instanceId: z.string(),
|
|
539
|
+
workflowName: z.string(),
|
|
540
|
+
error: z.string(),
|
|
541
|
+
}),
|
|
542
|
+
},
|
|
543
|
+
handle: (input, ctx) => {
|
|
544
|
+
if (!checkAuth(ctx)) {
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
// Subscribe to the global workflow events channel
|
|
548
|
+
// Events are emitted via ctx.core.events and need to be bridged to SSE
|
|
549
|
+
return ["workflows:all"];
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
|
|
400
553
|
// Audit list route
|
|
401
554
|
router.route("audit.list").typed(
|
|
402
555
|
defineRoute({
|
package/src/core/cron.ts
CHANGED
|
@@ -35,8 +35,16 @@ export interface Cron {
|
|
|
35
35
|
// Supports: * (any), specific values, ranges (1-5), steps (*/5)
|
|
36
36
|
// Format: second minute hour dayOfMonth month dayOfWeek
|
|
37
37
|
// Also supports 5-field format (minute hour dayOfMonth month dayOfWeek)
|
|
38
|
+
//
|
|
39
|
+
// Day-of-month/day-of-week semantics:
|
|
40
|
+
// - Standard cron uses OR semantics when both are restricted (not *)
|
|
41
|
+
// - If BOTH day-of-month and day-of-week are explicitly set, job runs if EITHER matches
|
|
42
|
+
// - If one is *, the other is used exclusively
|
|
38
43
|
class CronExpression {
|
|
39
44
|
private fields: [number[], number[], number[], number[], number[], number[]];
|
|
45
|
+
// Track if day fields were explicitly set (not *)
|
|
46
|
+
private dayOfMonthRestricted: boolean;
|
|
47
|
+
private dayOfWeekRestricted: boolean;
|
|
40
48
|
|
|
41
49
|
constructor(expression: string) {
|
|
42
50
|
const parts = expression.trim().split(/\s+/);
|
|
@@ -44,6 +52,8 @@ class CronExpression {
|
|
|
44
52
|
// Support both 5-field and 6-field cron
|
|
45
53
|
if (parts.length === 5) {
|
|
46
54
|
// minute hour dayOfMonth month dayOfWeek
|
|
55
|
+
this.dayOfMonthRestricted = parts[2] !== "*";
|
|
56
|
+
this.dayOfWeekRestricted = parts[4] !== "*";
|
|
47
57
|
this.fields = [
|
|
48
58
|
[0], // seconds (always 0)
|
|
49
59
|
this.parseField(parts[0]!, 0, 59), // minutes
|
|
@@ -54,6 +64,8 @@ class CronExpression {
|
|
|
54
64
|
];
|
|
55
65
|
} else if (parts.length === 6) {
|
|
56
66
|
// second minute hour dayOfMonth month dayOfWeek
|
|
67
|
+
this.dayOfMonthRestricted = parts[3] !== "*";
|
|
68
|
+
this.dayOfWeekRestricted = parts[5] !== "*";
|
|
57
69
|
this.fields = [
|
|
58
70
|
this.parseField(parts[0]!, 0, 59), // seconds
|
|
59
71
|
this.parseField(parts[1]!, 0, 59), // minutes
|
|
@@ -99,14 +111,25 @@ class CronExpression {
|
|
|
99
111
|
const month = date.getMonth() + 1;
|
|
100
112
|
const dayOfWeek = date.getDay();
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
114
|
+
// Check time fields (always AND)
|
|
115
|
+
if (!this.fields[0].includes(second)) return false;
|
|
116
|
+
if (!this.fields[1].includes(minute)) return false;
|
|
117
|
+
if (!this.fields[2].includes(hour)) return false;
|
|
118
|
+
if (!this.fields[4].includes(month)) return false;
|
|
119
|
+
|
|
120
|
+
// Day matching uses standard cron semantics:
|
|
121
|
+
// - If both day-of-month AND day-of-week are restricted (not *), use OR
|
|
122
|
+
// - Otherwise, use AND (one or both are unrestricted)
|
|
123
|
+
const domMatches = this.fields[3].includes(dayOfMonth);
|
|
124
|
+
const dowMatches = this.fields[5].includes(dayOfWeek);
|
|
125
|
+
|
|
126
|
+
if (this.dayOfMonthRestricted && this.dayOfWeekRestricted) {
|
|
127
|
+
// Both restricted: OR semantics - job runs if EITHER matches
|
|
128
|
+
return domMatches || dowMatches;
|
|
129
|
+
} else {
|
|
130
|
+
// One or both unrestricted: AND semantics
|
|
131
|
+
return domMatches && dowMatches;
|
|
132
|
+
}
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
/**
|
|
@@ -150,9 +173,21 @@ class CronExpression {
|
|
|
150
173
|
if (dayOfMonth > daysInMonth) continue; // Skip invalid days for this month
|
|
151
174
|
|
|
152
175
|
// Check if this day matches day-of-week constraint
|
|
176
|
+
// Use OR semantics if both are restricted, AND otherwise
|
|
153
177
|
const testDate = new Date(next.getFullYear(), targetMonth, dayOfMonth);
|
|
154
178
|
const dayOfWeek = testDate.getDay();
|
|
155
|
-
|
|
179
|
+
const domMatches = daysOfMonth.includes(dayOfMonth);
|
|
180
|
+
const dowMatches = daysOfWeek.includes(dayOfWeek);
|
|
181
|
+
|
|
182
|
+
let dayMatches: boolean;
|
|
183
|
+
if (this.dayOfMonthRestricted && this.dayOfWeekRestricted) {
|
|
184
|
+
// Both restricted: OR semantics
|
|
185
|
+
dayMatches = domMatches || dowMatches;
|
|
186
|
+
} else {
|
|
187
|
+
// One or both unrestricted: AND semantics
|
|
188
|
+
dayMatches = domMatches && dowMatches;
|
|
189
|
+
}
|
|
190
|
+
if (!dayMatches) continue;
|
|
156
191
|
|
|
157
192
|
// Skip days in the past
|
|
158
193
|
if (testDate < new Date(from.getFullYear(), from.getMonth(), from.getDate())) continue;
|
|
@@ -308,6 +343,9 @@ class CronImpl implements Cron {
|
|
|
308
343
|
if (this.running) return;
|
|
309
344
|
this.running = true;
|
|
310
345
|
|
|
346
|
+
// Catch-up check for missed runs since last shutdown
|
|
347
|
+
this.catchUpMissedRuns();
|
|
348
|
+
|
|
311
349
|
// Check every second
|
|
312
350
|
this.timer = setInterval(() => {
|
|
313
351
|
const now = new Date();
|
|
@@ -329,6 +367,49 @@ class CronImpl implements Cron {
|
|
|
329
367
|
}, 1000);
|
|
330
368
|
}
|
|
331
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Catch up on missed runs since last shutdown.
|
|
372
|
+
* For each task with a lastRun timestamp, execute any runs that were missed.
|
|
373
|
+
* Limited to 10 catch-up runs per task to avoid flooding.
|
|
374
|
+
*/
|
|
375
|
+
private catchUpMissedRuns(): void {
|
|
376
|
+
const now = new Date();
|
|
377
|
+
const maxCatchUp = 10;
|
|
378
|
+
|
|
379
|
+
for (const task of this.tasks.values()) {
|
|
380
|
+
if (!task.enabled || !task.lastRun) continue;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const cronExpr = new CronExpression(task.expression);
|
|
384
|
+
let missedRun = cronExpr.getNextRun(task.lastRun);
|
|
385
|
+
let catchUpCount = 0;
|
|
386
|
+
|
|
387
|
+
// Execute any missed runs (up to limit to avoid flooding)
|
|
388
|
+
while (missedRun && missedRun < now && catchUpCount < maxCatchUp) {
|
|
389
|
+
console.log(`[Cron] Catching up missed run for "${task.name}" at ${missedRun.toISOString()}`);
|
|
390
|
+
|
|
391
|
+
// Execute the handler asynchronously
|
|
392
|
+
Promise.resolve(task.handler()).catch(err => {
|
|
393
|
+
console.error(`[Cron] Catch-up task "${task.name}" failed:`, err);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
task.lastRun = missedRun;
|
|
397
|
+
missedRun = cronExpr.getNextRun(missedRun);
|
|
398
|
+
catchUpCount++;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Update nextRun to the next scheduled time
|
|
402
|
+
task.nextRun = cronExpr.getNextRun(now);
|
|
403
|
+
|
|
404
|
+
if (catchUpCount > 0) {
|
|
405
|
+
console.log(`[Cron] Caught up ${catchUpCount} missed runs for "${task.name}"`);
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.error(`[Cron] Error catching up task "${task.name}":`, err);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
332
413
|
async stop(): Promise<void> {
|
|
333
414
|
this.running = false;
|
|
334
415
|
if (this.timer) {
|
package/src/core/index.ts
CHANGED
|
@@ -132,6 +132,7 @@ export {
|
|
|
132
132
|
export {
|
|
133
133
|
type Workflows,
|
|
134
134
|
type WorkflowsConfig,
|
|
135
|
+
type WorkflowRegisterOptions,
|
|
135
136
|
type WorkflowDefinition,
|
|
136
137
|
type WorkflowInstance,
|
|
137
138
|
type WorkflowStatus,
|
|
@@ -154,6 +155,30 @@ export {
|
|
|
154
155
|
createWorkflows,
|
|
155
156
|
} from "./workflows";
|
|
156
157
|
|
|
158
|
+
export {
|
|
159
|
+
type WorkflowSocketServer,
|
|
160
|
+
type WorkflowSocketServerOptions,
|
|
161
|
+
type WorkflowSocketConfig,
|
|
162
|
+
type WorkflowEvent,
|
|
163
|
+
type WorkflowEventType,
|
|
164
|
+
type ProxyRequest,
|
|
165
|
+
type ProxyResponse,
|
|
166
|
+
type WorkflowMessage,
|
|
167
|
+
createWorkflowSocketServer,
|
|
168
|
+
isWorkflowEvent,
|
|
169
|
+
isProxyRequest,
|
|
170
|
+
parseWorkflowMessage,
|
|
171
|
+
} from "./workflow-socket";
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
type ProxyConnection,
|
|
175
|
+
WorkflowProxyConnection,
|
|
176
|
+
createPluginProxy,
|
|
177
|
+
createCoreProxy,
|
|
178
|
+
createPluginsProxy,
|
|
179
|
+
createCoreServicesProxy,
|
|
180
|
+
} from "./workflow-proxy";
|
|
181
|
+
|
|
157
182
|
export {
|
|
158
183
|
type Processes,
|
|
159
184
|
type ProcessesConfig,
|
|
@@ -192,6 +217,12 @@ export {
|
|
|
192
217
|
createProcessSocketServer,
|
|
193
218
|
} from "./process-socket";
|
|
194
219
|
|
|
220
|
+
export {
|
|
221
|
+
WorkflowStateMachine,
|
|
222
|
+
type StateMachineEvents,
|
|
223
|
+
type StateMachineConfig,
|
|
224
|
+
} from "./workflow-state-machine";
|
|
225
|
+
|
|
195
226
|
export {
|
|
196
227
|
KyselyWorkflowAdapter,
|
|
197
228
|
type KyselyWorkflowAdapterConfig,
|