@donkeylabs/server 2.0.22 → 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 +28 -2
- package/docs/jobs.md +15 -0
- package/docs/workflows.md +60 -1
- package/package.json +1 -1
- package/src/core/cron.ts +50 -7
- package/src/core/index.ts +3 -0
- package/src/core/jobs.ts +42 -3
- package/src/core/subprocess-bootstrap.ts +241 -0
- package/src/core/workflow-executor.ts +48 -43
- package/src/core/workflow-socket.ts +1 -0
- package/src/core/workflow-state-machine.ts +34 -1
- package/src/core/workflows.ts +291 -11
- package/src/core.ts +81 -3
- package/src/server.ts +10 -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
|
@@ -487,6 +487,62 @@ ctx.core.workflows.register(myWorkflow);
|
|
|
487
487
|
|
|
488
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
489
|
|
|
490
|
+
#### Isolated Plugin Initialization
|
|
491
|
+
|
|
492
|
+
In isolated mode, the subprocess **boots a full plugin manager** and runs plugin `init` hooks locally. This means your workflow handlers can use `ctx.plugins` without IPC fallbacks, and cron/jobs/workflows/services registered in `init` are available inside the subprocess.
|
|
493
|
+
|
|
494
|
+
Requirements:
|
|
495
|
+
- Plugin modules must be discoverable from their module path (captured during `createPlugin.define()` / `pluginFactory()` calls).
|
|
496
|
+
- Plugin configs and `ctx.core.config` must be JSON-serializable.
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
// plugins/reports/index.ts
|
|
500
|
+
export const reportsPlugin = createPlugin.define({
|
|
501
|
+
name: "reports",
|
|
502
|
+
service: async (ctx) => ({
|
|
503
|
+
generate: async (id: string) => ctx.db.selectFrom("reports").selectAll().execute(),
|
|
504
|
+
}),
|
|
505
|
+
init: async (ctx) => {
|
|
506
|
+
ctx.core.jobs.register("reports.generate", async () => undefined);
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// workflows/report.ts
|
|
511
|
+
export const reportWorkflow = workflow("report.generate")
|
|
512
|
+
.task("run", {
|
|
513
|
+
handler: async (input, ctx) => {
|
|
514
|
+
const data = await ctx.plugins.reports.generate(input.reportId);
|
|
515
|
+
return { data };
|
|
516
|
+
},
|
|
517
|
+
})
|
|
518
|
+
.build();
|
|
519
|
+
```
|
|
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
|
+
|
|
490
546
|
### Inline Mode
|
|
491
547
|
|
|
492
548
|
For lightweight workflows that complete quickly, you can opt into inline execution:
|
|
@@ -512,7 +568,7 @@ ctx.core.workflows.register(quickWorkflow);
|
|
|
512
568
|
|---|---|---|
|
|
513
569
|
| Step types | All (task, choice, parallel, pass) | All (task, choice, parallel, pass) |
|
|
514
570
|
| Event loop | Separate process, won't block server | Runs on main thread |
|
|
515
|
-
| Plugin access |
|
|
571
|
+
| Plugin access | Local plugin services in subprocess | Direct access |
|
|
516
572
|
| Best for | Long-running, CPU-intensive workflows | Quick validations, lightweight flows |
|
|
517
573
|
| Setup | `workflows.register(wf)` | `workflows.register(wf)` |
|
|
518
574
|
|
|
@@ -545,6 +601,9 @@ interface Workflows {
|
|
|
545
601
|
|
|
546
602
|
/** Stop the workflow service */
|
|
547
603
|
stop(): Promise<void>;
|
|
604
|
+
|
|
605
|
+
/** Set plugin metadata for isolated workflows (AppServer sets this automatically) */
|
|
606
|
+
setPluginMetadata(metadata: PluginMetadata): void;
|
|
548
607
|
}
|
|
549
608
|
|
|
550
609
|
interface WorkflowRegisterOptions {
|
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,
|
|
@@ -149,6 +151,7 @@ export {
|
|
|
149
151
|
type PassStepDefinition,
|
|
150
152
|
type RetryConfig,
|
|
151
153
|
type GetAllWorkflowsOptions,
|
|
154
|
+
type PluginMetadata,
|
|
152
155
|
WorkflowBuilder,
|
|
153
156
|
MemoryWorkflowAdapter,
|
|
154
157
|
workflow,
|
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 {
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
3
|
+
import Database from "bun:sqlite";
|
|
4
|
+
import {
|
|
5
|
+
createLogger,
|
|
6
|
+
createCache,
|
|
7
|
+
createEvents,
|
|
8
|
+
createCron,
|
|
9
|
+
createJobs,
|
|
10
|
+
createSSE,
|
|
11
|
+
createRateLimiter,
|
|
12
|
+
createErrors,
|
|
13
|
+
createWorkflows,
|
|
14
|
+
createProcesses,
|
|
15
|
+
createAudit,
|
|
16
|
+
createWebSocket,
|
|
17
|
+
createStorage,
|
|
18
|
+
createLogs,
|
|
19
|
+
KyselyJobAdapter,
|
|
20
|
+
KyselyWorkflowAdapter,
|
|
21
|
+
MemoryAuditAdapter,
|
|
22
|
+
MemoryLogsAdapter,
|
|
23
|
+
} from "./index";
|
|
24
|
+
import { PluginManager, type CoreServices, type ConfiguredPlugin } from "../core";
|
|
25
|
+
|
|
26
|
+
export interface SubprocessPluginMetadata {
|
|
27
|
+
names: string[];
|
|
28
|
+
modulePaths: Record<string, string>;
|
|
29
|
+
configs: Record<string, any>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SubprocessBootstrapOptions {
|
|
33
|
+
dbPath: string;
|
|
34
|
+
coreConfig?: Record<string, any>;
|
|
35
|
+
pluginMetadata: SubprocessPluginMetadata;
|
|
36
|
+
startServices?: {
|
|
37
|
+
cron?: boolean;
|
|
38
|
+
jobs?: boolean;
|
|
39
|
+
workflows?: boolean;
|
|
40
|
+
processes?: boolean;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SubprocessBootstrapResult {
|
|
45
|
+
core: CoreServices;
|
|
46
|
+
manager: PluginManager;
|
|
47
|
+
db: Kysely<any>;
|
|
48
|
+
workflowAdapter: KyselyWorkflowAdapter;
|
|
49
|
+
cleanup: () => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function bootstrapSubprocess(
|
|
53
|
+
options: SubprocessBootstrapOptions
|
|
54
|
+
): Promise<SubprocessBootstrapResult> {
|
|
55
|
+
const sqlite = new Database(options.dbPath);
|
|
56
|
+
sqlite.run("PRAGMA busy_timeout = 5000");
|
|
57
|
+
|
|
58
|
+
const db = new Kysely<any>({
|
|
59
|
+
dialect: new BunSqliteDialect({ database: sqlite }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const cache = createCache();
|
|
63
|
+
const events = createEvents();
|
|
64
|
+
const sse = createSSE();
|
|
65
|
+
const rateLimiter = createRateLimiter();
|
|
66
|
+
const errors = createErrors();
|
|
67
|
+
|
|
68
|
+
const logs = createLogs({ adapter: new MemoryLogsAdapter(), events });
|
|
69
|
+
const logger = createLogger();
|
|
70
|
+
|
|
71
|
+
const cron = createCron({ logger, events });
|
|
72
|
+
|
|
73
|
+
const jobAdapter = new KyselyJobAdapter(db, { cleanupDays: 0 });
|
|
74
|
+
const workflowAdapter = new KyselyWorkflowAdapter(db, { cleanupDays: 0 });
|
|
75
|
+
const auditAdapter = new MemoryAuditAdapter();
|
|
76
|
+
|
|
77
|
+
const jobs = createJobs({
|
|
78
|
+
events,
|
|
79
|
+
logger,
|
|
80
|
+
adapter: jobAdapter,
|
|
81
|
+
persist: false,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const workflows = createWorkflows({
|
|
85
|
+
events,
|
|
86
|
+
jobs,
|
|
87
|
+
sse,
|
|
88
|
+
adapter: workflowAdapter,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const processes = createProcesses({ events, autoRecoverOrphans: false });
|
|
92
|
+
const audit = createAudit({ adapter: auditAdapter });
|
|
93
|
+
const websocket = createWebSocket();
|
|
94
|
+
const storage = createStorage();
|
|
95
|
+
|
|
96
|
+
const core: CoreServices = {
|
|
97
|
+
db,
|
|
98
|
+
config: options.coreConfig ?? {},
|
|
99
|
+
logger,
|
|
100
|
+
cache,
|
|
101
|
+
events,
|
|
102
|
+
cron,
|
|
103
|
+
jobs,
|
|
104
|
+
sse,
|
|
105
|
+
rateLimiter,
|
|
106
|
+
errors,
|
|
107
|
+
workflows,
|
|
108
|
+
processes,
|
|
109
|
+
audit,
|
|
110
|
+
websocket,
|
|
111
|
+
storage,
|
|
112
|
+
logs,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
workflows.setCore(core);
|
|
116
|
+
|
|
117
|
+
const manager = new PluginManager(core);
|
|
118
|
+
const plugins = await loadConfiguredPlugins(options.pluginMetadata);
|
|
119
|
+
|
|
120
|
+
for (const plugin of plugins) {
|
|
121
|
+
manager.register(plugin);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await manager.init();
|
|
125
|
+
workflows.setPlugins(manager.getServices());
|
|
126
|
+
|
|
127
|
+
if (options.startServices?.cron) {
|
|
128
|
+
core.cron.start();
|
|
129
|
+
}
|
|
130
|
+
if (options.startServices?.jobs) {
|
|
131
|
+
core.jobs.start();
|
|
132
|
+
}
|
|
133
|
+
if (options.startServices?.workflows) {
|
|
134
|
+
await core.workflows.resolveDbPath();
|
|
135
|
+
await core.workflows.resume();
|
|
136
|
+
}
|
|
137
|
+
if (options.startServices?.processes) {
|
|
138
|
+
core.processes.start();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const cleanup = async () => {
|
|
142
|
+
await core.cron.stop();
|
|
143
|
+
await core.jobs.stop();
|
|
144
|
+
await core.workflows.stop();
|
|
145
|
+
await core.processes.shutdown();
|
|
146
|
+
|
|
147
|
+
if (typeof (logs as any).stop === "function") {
|
|
148
|
+
(logs as any).stop();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof (audit as any).stop === "function") {
|
|
152
|
+
(audit as any).stop();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await db.destroy();
|
|
156
|
+
sqlite.close();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return { core, manager, db, workflowAdapter, cleanup };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadConfiguredPlugins(
|
|
163
|
+
metadata: SubprocessPluginMetadata
|
|
164
|
+
): Promise<ConfiguredPlugin[]> {
|
|
165
|
+
const plugins: ConfiguredPlugin[] = [];
|
|
166
|
+
|
|
167
|
+
for (const name of metadata.names) {
|
|
168
|
+
const modulePath = metadata.modulePaths[name];
|
|
169
|
+
if (!modulePath) {
|
|
170
|
+
throw new Error(`Missing module path for plugin "${name}"`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const module = await import(modulePath);
|
|
174
|
+
const config = metadata.configs?.[name];
|
|
175
|
+
const plugin = findPluginDefinition(module, name, config);
|
|
176
|
+
|
|
177
|
+
if (!plugin) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Plugin "${name}" not found in module ${modulePath}. ` +
|
|
180
|
+
`Ensure the plugin is exported and its config is serializable.`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
plugins.push(plugin);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return plugins;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findPluginDefinition(
|
|
191
|
+
mod: any,
|
|
192
|
+
pluginName: string,
|
|
193
|
+
boundConfig?: any
|
|
194
|
+
): ConfiguredPlugin | null {
|
|
195
|
+
for (const key of Object.keys(mod)) {
|
|
196
|
+
const exported = mod[key];
|
|
197
|
+
const direct = resolvePluginDefinition(exported, pluginName, boundConfig);
|
|
198
|
+
if (direct) return direct;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (mod.default) {
|
|
202
|
+
const direct = resolvePluginDefinition(mod.default, pluginName, boundConfig);
|
|
203
|
+
if (direct) return direct;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolvePluginDefinition(
|
|
210
|
+
exported: any,
|
|
211
|
+
pluginName: string,
|
|
212
|
+
boundConfig?: any
|
|
213
|
+
): ConfiguredPlugin | null {
|
|
214
|
+
if (!exported) return null;
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
typeof exported === "object" &&
|
|
218
|
+
exported.name === pluginName &&
|
|
219
|
+
typeof exported.service === "function"
|
|
220
|
+
) {
|
|
221
|
+
return exported as ConfiguredPlugin;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (typeof exported === "function" && boundConfig !== undefined) {
|
|
225
|
+
try {
|
|
226
|
+
const result = exported(boundConfig);
|
|
227
|
+
if (
|
|
228
|
+
result &&
|
|
229
|
+
typeof result === "object" &&
|
|
230
|
+
result.name === pluginName &&
|
|
231
|
+
typeof result.service === "function"
|
|
232
|
+
) {
|
|
233
|
+
return result as ConfiguredPlugin;
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|