@generacy-ai/generacy 0.0.0-preview-20260304013206
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/LICENSE +191 -0
- package/README.md +207 -0
- package/bin/generacy.js +11 -0
- package/dist/agency/index.d.ts +68 -0
- package/dist/agency/index.d.ts.map +1 -0
- package/dist/agency/index.js +28 -0
- package/dist/agency/index.js.map +1 -0
- package/dist/agency/network.d.ts +41 -0
- package/dist/agency/network.d.ts.map +1 -0
- package/dist/agency/network.js +133 -0
- package/dist/agency/network.js.map +1 -0
- package/dist/agency/subprocess.d.ts +58 -0
- package/dist/agency/subprocess.d.ts.map +1 -0
- package/dist/agency/subprocess.js +216 -0
- package/dist/agency/subprocess.js.map +1 -0
- package/dist/cli/commands/agent.d.ts +10 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +216 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/doctor/checks/agency-mcp.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/agency-mcp.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/agency-mcp.js +51 -0
- package/dist/cli/commands/doctor/checks/agency-mcp.js.map +1 -0
- package/dist/cli/commands/doctor/checks/anthropic-key.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/anthropic-key.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/anthropic-key.js +68 -0
- package/dist/cli/commands/doctor/checks/anthropic-key.js.map +1 -0
- package/dist/cli/commands/doctor/checks/config.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/config.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/config.js +81 -0
- package/dist/cli/commands/doctor/checks/config.js.map +1 -0
- package/dist/cli/commands/doctor/checks/devcontainer.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/devcontainer.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/devcontainer.js +58 -0
- package/dist/cli/commands/doctor/checks/devcontainer.js.map +1 -0
- package/dist/cli/commands/doctor/checks/docker.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/docker.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/docker.js +71 -0
- package/dist/cli/commands/doctor/checks/docker.js.map +1 -0
- package/dist/cli/commands/doctor/checks/env-file.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/env-file.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/env-file.js +56 -0
- package/dist/cli/commands/doctor/checks/env-file.js.map +1 -0
- package/dist/cli/commands/doctor/checks/github-token.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/github-token.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/github-token.js +99 -0
- package/dist/cli/commands/doctor/checks/github-token.js.map +1 -0
- package/dist/cli/commands/doctor/checks/npm-packages.d.ts +3 -0
- package/dist/cli/commands/doctor/checks/npm-packages.d.ts.map +1 -0
- package/dist/cli/commands/doctor/checks/npm-packages.js +117 -0
- package/dist/cli/commands/doctor/checks/npm-packages.js.map +1 -0
- package/dist/cli/commands/doctor/formatter.d.ts +27 -0
- package/dist/cli/commands/doctor/formatter.d.ts.map +1 -0
- package/dist/cli/commands/doctor/formatter.js +162 -0
- package/dist/cli/commands/doctor/formatter.js.map +1 -0
- package/dist/cli/commands/doctor/index.d.ts +5 -0
- package/dist/cli/commands/doctor/index.d.ts.map +1 -0
- package/dist/cli/commands/doctor/index.js +8 -0
- package/dist/cli/commands/doctor/index.js.map +1 -0
- package/dist/cli/commands/doctor/registry.d.ts +48 -0
- package/dist/cli/commands/doctor/registry.d.ts.map +1 -0
- package/dist/cli/commands/doctor/registry.js +166 -0
- package/dist/cli/commands/doctor/registry.js.map +1 -0
- package/dist/cli/commands/doctor/runner.d.ts +14 -0
- package/dist/cli/commands/doctor/runner.d.ts.map +1 -0
- package/dist/cli/commands/doctor/runner.js +257 -0
- package/dist/cli/commands/doctor/runner.js.map +1 -0
- package/dist/cli/commands/doctor/types.d.ts +87 -0
- package/dist/cli/commands/doctor/types.d.ts.map +1 -0
- package/dist/cli/commands/doctor/types.js +2 -0
- package/dist/cli/commands/doctor/types.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +12 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +97 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init/conflicts.d.ts +36 -0
- package/dist/cli/commands/init/conflicts.d.ts.map +1 -0
- package/dist/cli/commands/init/conflicts.js +165 -0
- package/dist/cli/commands/init/conflicts.js.map +1 -0
- package/dist/cli/commands/init/github.d.ts +32 -0
- package/dist/cli/commands/init/github.d.ts.map +1 -0
- package/dist/cli/commands/init/github.js +161 -0
- package/dist/cli/commands/init/github.js.map +1 -0
- package/dist/cli/commands/init/index.d.ts +21 -0
- package/dist/cli/commands/init/index.d.ts.map +1 -0
- package/dist/cli/commands/init/index.js +175 -0
- package/dist/cli/commands/init/index.js.map +1 -0
- package/dist/cli/commands/init/prompts.d.ts +15 -0
- package/dist/cli/commands/init/prompts.d.ts.map +1 -0
- package/dist/cli/commands/init/prompts.js +281 -0
- package/dist/cli/commands/init/prompts.js.map +1 -0
- package/dist/cli/commands/init/repo-utils.d.ts +32 -0
- package/dist/cli/commands/init/repo-utils.d.ts.map +1 -0
- package/dist/cli/commands/init/repo-utils.js +112 -0
- package/dist/cli/commands/init/repo-utils.js.map +1 -0
- package/dist/cli/commands/init/resolver.d.ts +20 -0
- package/dist/cli/commands/init/resolver.d.ts.map +1 -0
- package/dist/cli/commands/init/resolver.js +273 -0
- package/dist/cli/commands/init/resolver.js.map +1 -0
- package/dist/cli/commands/init/summary.d.ts +21 -0
- package/dist/cli/commands/init/summary.d.ts.map +1 -0
- package/dist/cli/commands/init/summary.js +100 -0
- package/dist/cli/commands/init/summary.js.map +1 -0
- package/dist/cli/commands/init/types.d.ts +53 -0
- package/dist/cli/commands/init/types.d.ts.map +1 -0
- package/dist/cli/commands/init/types.js +2 -0
- package/dist/cli/commands/init/types.js.map +1 -0
- package/dist/cli/commands/init/writer.d.ts +22 -0
- package/dist/cli/commands/init/writer.d.ts.map +1 -0
- package/dist/cli/commands/init/writer.js +96 -0
- package/dist/cli/commands/init/writer.js.map +1 -0
- package/dist/cli/commands/orchestrator.d.ts +11 -0
- package/dist/cli/commands/orchestrator.d.ts.map +1 -0
- package/dist/cli/commands/orchestrator.js +291 -0
- package/dist/cli/commands/orchestrator.js.map +1 -0
- package/dist/cli/commands/run.d.ts +10 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +167 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/setup/auth.d.ts +11 -0
- package/dist/cli/commands/setup/auth.d.ts.map +1 -0
- package/dist/cli/commands/setup/auth.js +108 -0
- package/dist/cli/commands/setup/auth.js.map +1 -0
- package/dist/cli/commands/setup/build.d.ts +11 -0
- package/dist/cli/commands/setup/build.d.ts.map +1 -0
- package/dist/cli/commands/setup/build.js +212 -0
- package/dist/cli/commands/setup/build.js.map +1 -0
- package/dist/cli/commands/setup/services.d.ts +11 -0
- package/dist/cli/commands/setup/services.d.ts.map +1 -0
- package/dist/cli/commands/setup/services.js +294 -0
- package/dist/cli/commands/setup/services.js.map +1 -0
- package/dist/cli/commands/setup/workspace.d.ts +11 -0
- package/dist/cli/commands/setup/workspace.d.ts.map +1 -0
- package/dist/cli/commands/setup/workspace.js +215 -0
- package/dist/cli/commands/setup/workspace.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +7 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +19 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +10 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +164 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/commands/worker.d.ts +10 -0
- package/dist/cli/commands/worker.d.ts.map +1 -0
- package/dist/cli/commands/worker.js +224 -0
- package/dist/cli/commands/worker.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +68 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/config.d.ts +49 -0
- package/dist/cli/utils/config.d.ts.map +1 -0
- package/dist/cli/utils/config.js +110 -0
- package/dist/cli/utils/config.js.map +1 -0
- package/dist/cli/utils/exec.d.ts +39 -0
- package/dist/cli/utils/exec.d.ts.map +1 -0
- package/dist/cli/utils/exec.js +68 -0
- package/dist/cli/utils/exec.js.map +1 -0
- package/dist/cli/utils/logger.d.ts +47 -0
- package/dist/cli/utils/logger.d.ts.map +1 -0
- package/dist/cli/utils/logger.js +97 -0
- package/dist/cli/utils/logger.js.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +13 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +104 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +266 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +304 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +160 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/validator.d.ts +60 -0
- package/dist/config/validator.d.ts.map +1 -0
- package/dist/config/validator.js +112 -0
- package/dist/config/validator.js.map +1 -0
- package/dist/health/server.d.ts +47 -0
- package/dist/health/server.d.ts.map +1 -0
- package/dist/health/server.js +92 -0
- package/dist/health/server.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/async-event-queue.d.ts +28 -0
- package/dist/orchestrator/async-event-queue.d.ts.map +1 -0
- package/dist/orchestrator/async-event-queue.js +57 -0
- package/dist/orchestrator/async-event-queue.js.map +1 -0
- package/dist/orchestrator/client.d.ts +110 -0
- package/dist/orchestrator/client.d.ts.map +1 -0
- package/dist/orchestrator/client.js +288 -0
- package/dist/orchestrator/client.js.map +1 -0
- package/dist/orchestrator/event-bus.d.ts +195 -0
- package/dist/orchestrator/event-bus.d.ts.map +1 -0
- package/dist/orchestrator/event-bus.js +557 -0
- package/dist/orchestrator/event-bus.js.map +1 -0
- package/dist/orchestrator/heartbeat.d.ts +71 -0
- package/dist/orchestrator/heartbeat.d.ts.map +1 -0
- package/dist/orchestrator/heartbeat.js +116 -0
- package/dist/orchestrator/heartbeat.js.map +1 -0
- package/dist/orchestrator/index.d.ts +25 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/index.js +15 -0
- package/dist/orchestrator/index.js.map +1 -0
- package/dist/orchestrator/job-handler.d.ts +109 -0
- package/dist/orchestrator/job-handler.d.ts.map +1 -0
- package/dist/orchestrator/job-handler.js +612 -0
- package/dist/orchestrator/job-handler.js.map +1 -0
- package/dist/orchestrator/job-queue.d.ts +81 -0
- package/dist/orchestrator/job-queue.d.ts.map +1 -0
- package/dist/orchestrator/job-queue.js +206 -0
- package/dist/orchestrator/job-queue.js.map +1 -0
- package/dist/orchestrator/label-monitor-bridge.d.ts +25 -0
- package/dist/orchestrator/label-monitor-bridge.d.ts.map +1 -0
- package/dist/orchestrator/label-monitor-bridge.js +57 -0
- package/dist/orchestrator/label-monitor-bridge.js.map +1 -0
- package/dist/orchestrator/log-buffer.d.ts +74 -0
- package/dist/orchestrator/log-buffer.d.ts.map +1 -0
- package/dist/orchestrator/log-buffer.js +104 -0
- package/dist/orchestrator/log-buffer.js.map +1 -0
- package/dist/orchestrator/redis-job-queue.d.ts +44 -0
- package/dist/orchestrator/redis-job-queue.d.ts.map +1 -0
- package/dist/orchestrator/redis-job-queue.js +300 -0
- package/dist/orchestrator/redis-job-queue.js.map +1 -0
- package/dist/orchestrator/router.d.ts +125 -0
- package/dist/orchestrator/router.d.ts.map +1 -0
- package/dist/orchestrator/router.js +143 -0
- package/dist/orchestrator/router.js.map +1 -0
- package/dist/orchestrator/server.d.ts +62 -0
- package/dist/orchestrator/server.d.ts.map +1 -0
- package/dist/orchestrator/server.js +711 -0
- package/dist/orchestrator/server.js.map +1 -0
- package/dist/orchestrator/types.d.ts +184 -0
- package/dist/orchestrator/types.d.ts.map +1 -0
- package/dist/orchestrator/types.js +6 -0
- package/dist/orchestrator/types.js.map +1 -0
- package/dist/orchestrator/worker-registry.d.ts +110 -0
- package/dist/orchestrator/worker-registry.d.ts.map +1 -0
- package/dist/orchestrator/worker-registry.js +191 -0
- package/dist/orchestrator/worker-registry.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator HTTP server.
|
|
3
|
+
* Provides REST API for worker registration, job distribution, and health monitoring.
|
|
4
|
+
*/
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { WorkerRegistry } from './worker-registry.js';
|
|
8
|
+
import { InMemoryJobQueue } from './job-queue.js';
|
|
9
|
+
import { createRouter, pathToRegex, parseJsonBody, sendJson, sendError } from './router.js';
|
|
10
|
+
import { EventBus } from './event-bus.js';
|
|
11
|
+
import { LogBufferManager } from './log-buffer.js';
|
|
12
|
+
/**
|
|
13
|
+
* Default logger that writes to console
|
|
14
|
+
*/
|
|
15
|
+
const defaultLogger = {
|
|
16
|
+
info: (message, data) => {
|
|
17
|
+
console.log(`[INFO] ${message}`, data ? JSON.stringify(data) : '');
|
|
18
|
+
},
|
|
19
|
+
warn: (message, data) => {
|
|
20
|
+
console.warn(`[WARN] ${message}`, data ? JSON.stringify(data) : '');
|
|
21
|
+
},
|
|
22
|
+
error: (message, data) => {
|
|
23
|
+
console.error(`[ERROR] ${message}`, data ? JSON.stringify(data) : '');
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Create an orchestrator server
|
|
28
|
+
*/
|
|
29
|
+
export function createOrchestratorServer(options = {}) {
|
|
30
|
+
const { port = 3100, host = '0.0.0.0', workerTimeout = 60000, authToken = process.env['ORCHESTRATOR_TOKEN'], eventBufferSize = 1000, eventGracePeriod = 300000, sseHeartbeatInterval = 30000, logger = defaultLogger, } = options;
|
|
31
|
+
// Initialize components
|
|
32
|
+
const workerRegistry = new WorkerRegistry({
|
|
33
|
+
heartbeatTimeout: workerTimeout,
|
|
34
|
+
onWorkerOffline: (workerId) => {
|
|
35
|
+
logger.warn('Worker went offline', { workerId });
|
|
36
|
+
},
|
|
37
|
+
onWorkerUnhealthy: (workerId) => {
|
|
38
|
+
logger.warn('Worker became unhealthy', { workerId });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const jobQueue = options.jobQueue ?? new InMemoryJobQueue();
|
|
42
|
+
const logBufferManager = new LogBufferManager({
|
|
43
|
+
gracePeriod: eventGracePeriod,
|
|
44
|
+
});
|
|
45
|
+
const eventBus = new EventBus({
|
|
46
|
+
bufferSize: eventBufferSize,
|
|
47
|
+
gracePeriod: eventGracePeriod,
|
|
48
|
+
heartbeatInterval: sseHeartbeatInterval,
|
|
49
|
+
jobQueue,
|
|
50
|
+
logBufferManager,
|
|
51
|
+
logger,
|
|
52
|
+
});
|
|
53
|
+
// Start periodic timeout check
|
|
54
|
+
let timeoutInterval = null;
|
|
55
|
+
/**
|
|
56
|
+
* Check authentication header
|
|
57
|
+
*/
|
|
58
|
+
function authenticate(req) {
|
|
59
|
+
if (!authToken) {
|
|
60
|
+
return true; // Auth disabled
|
|
61
|
+
}
|
|
62
|
+
const authHeader = req.headers['authorization'];
|
|
63
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return authHeader.slice(7) === authToken;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build routes using pathToRegex helper
|
|
70
|
+
*/
|
|
71
|
+
const healthRoute = pathToRegex('/api/health');
|
|
72
|
+
const registerRoute = pathToRegex('/api/workers/register');
|
|
73
|
+
const unregisterRoute = pathToRegex('/api/workers/:workerId');
|
|
74
|
+
const heartbeatRoute = pathToRegex('/api/workers/:workerId/heartbeat');
|
|
75
|
+
const submitJobRoute = pathToRegex('/api/jobs');
|
|
76
|
+
const pollRoute = pathToRegex('/api/jobs/poll');
|
|
77
|
+
const getJobRoute = pathToRegex('/api/jobs/:jobId');
|
|
78
|
+
const statusRoute = pathToRegex('/api/jobs/:jobId/status');
|
|
79
|
+
const resultRoute = pathToRegex('/api/jobs/:jobId/result');
|
|
80
|
+
const cancelRoute = pathToRegex('/api/jobs/:jobId/cancel');
|
|
81
|
+
const jobEventsRoute = pathToRegex('/api/jobs/:jobId/events');
|
|
82
|
+
const jobLogsRoute = pathToRegex('/api/jobs/:jobId/logs');
|
|
83
|
+
const globalEventsRoute = pathToRegex('/api/events');
|
|
84
|
+
const router = createRouter([
|
|
85
|
+
{ method: 'GET', pattern: healthRoute.regex, handler: 'healthCheck', paramNames: healthRoute.paramNames },
|
|
86
|
+
{ method: 'POST', pattern: registerRoute.regex, handler: 'registerWorker', paramNames: registerRoute.paramNames },
|
|
87
|
+
{ method: 'DELETE', pattern: unregisterRoute.regex, handler: 'unregisterWorker', paramNames: unregisterRoute.paramNames },
|
|
88
|
+
{ method: 'POST', pattern: heartbeatRoute.regex, handler: 'handleHeartbeat', paramNames: heartbeatRoute.paramNames },
|
|
89
|
+
{ method: 'POST', pattern: submitJobRoute.regex, handler: 'submitJob', paramNames: submitJobRoute.paramNames },
|
|
90
|
+
{ method: 'GET', pattern: pollRoute.regex, handler: 'pollJob', paramNames: pollRoute.paramNames },
|
|
91
|
+
{ method: 'GET', pattern: globalEventsRoute.regex, handler: 'subscribeAllEvents', paramNames: globalEventsRoute.paramNames },
|
|
92
|
+
{ method: 'GET', pattern: jobLogsRoute.regex, handler: 'getJobLogs', paramNames: jobLogsRoute.paramNames },
|
|
93
|
+
{ method: 'GET', pattern: jobEventsRoute.regex, handler: 'subscribeJobEvents', paramNames: jobEventsRoute.paramNames },
|
|
94
|
+
{ method: 'POST', pattern: jobEventsRoute.regex, handler: 'publishEvent', paramNames: jobEventsRoute.paramNames },
|
|
95
|
+
{ method: 'GET', pattern: getJobRoute.regex, handler: 'getJob', paramNames: getJobRoute.paramNames },
|
|
96
|
+
{ method: 'PUT', pattern: statusRoute.regex, handler: 'updateJobStatus', paramNames: statusRoute.paramNames },
|
|
97
|
+
{ method: 'POST', pattern: resultRoute.regex, handler: 'reportResult', paramNames: resultRoute.paramNames },
|
|
98
|
+
{ method: 'POST', pattern: cancelRoute.regex, handler: 'cancelJob', paramNames: cancelRoute.paramNames },
|
|
99
|
+
]);
|
|
100
|
+
/**
|
|
101
|
+
* Route handlers
|
|
102
|
+
*/
|
|
103
|
+
const handlers = {
|
|
104
|
+
/**
|
|
105
|
+
* GET /api/health - Health check endpoint
|
|
106
|
+
*/
|
|
107
|
+
async healthCheck(_req, res) {
|
|
108
|
+
const workerCounts = workerRegistry.getWorkerCounts();
|
|
109
|
+
sendJson(res, 200, {
|
|
110
|
+
status: 'healthy',
|
|
111
|
+
workers: workerCounts.total,
|
|
112
|
+
healthyWorkers: workerCounts.healthy,
|
|
113
|
+
unhealthyWorkers: workerCounts.unhealthy,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* POST /api/jobs - Submit a new job
|
|
119
|
+
*/
|
|
120
|
+
async submitJob(req, res) {
|
|
121
|
+
try {
|
|
122
|
+
const body = await parseJsonBody(req);
|
|
123
|
+
if (!body.name) {
|
|
124
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: name');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!body.workflow) {
|
|
128
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: workflow');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const job = {
|
|
132
|
+
...body,
|
|
133
|
+
id: randomUUID(),
|
|
134
|
+
status: 'pending',
|
|
135
|
+
createdAt: new Date().toISOString(),
|
|
136
|
+
priority: body.priority ?? 'normal',
|
|
137
|
+
inputs: body.inputs ?? {},
|
|
138
|
+
};
|
|
139
|
+
await jobQueue.enqueue(job);
|
|
140
|
+
logger.info('Job submitted via API', { jobId: job.id, name: job.name });
|
|
141
|
+
sendJson(res, 201, { jobId: job.id, status: job.status });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof Error && error.message === 'Invalid JSON') {
|
|
145
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
logger.error('Error submitting job', { error: String(error) });
|
|
149
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'Failed to submit job');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
/**
|
|
154
|
+
* POST /api/workers/register - Register a worker
|
|
155
|
+
*/
|
|
156
|
+
async registerWorker(req, res) {
|
|
157
|
+
try {
|
|
158
|
+
const body = await parseJsonBody(req);
|
|
159
|
+
if (!body.name) {
|
|
160
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: name');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Check if worker ID already exists
|
|
164
|
+
if (body.id && workerRegistry.getWorker(body.id)) {
|
|
165
|
+
sendError(res, 409, 'WORKER_ALREADY_EXISTS', `Worker with ID ${body.id} already exists`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const workerId = await workerRegistry.register(body);
|
|
169
|
+
logger.info('Worker registered', { workerId, name: body.name });
|
|
170
|
+
sendJson(res, 200, { workerId });
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error instanceof Error && error.message === 'Invalid JSON') {
|
|
174
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
logger.error('Error registering worker', { error: String(error) });
|
|
178
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'Failed to register worker');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* DELETE /api/workers/:workerId - Unregister a worker
|
|
184
|
+
*/
|
|
185
|
+
async unregisterWorker(_req, res, params) {
|
|
186
|
+
const { workerId } = params;
|
|
187
|
+
if (!workerId) {
|
|
188
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing workerId parameter');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const worker = workerRegistry.getWorker(workerId);
|
|
192
|
+
if (!worker) {
|
|
193
|
+
sendError(res, 404, 'WORKER_NOT_FOUND', `Worker with ID ${workerId} not found`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await workerRegistry.unregister(workerId);
|
|
197
|
+
logger.info('Worker unregistered', { workerId });
|
|
198
|
+
res.writeHead(204);
|
|
199
|
+
res.end();
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* POST /api/workers/:workerId/heartbeat - Handle heartbeat
|
|
203
|
+
*/
|
|
204
|
+
async handleHeartbeat(req, res, params) {
|
|
205
|
+
const { workerId } = params;
|
|
206
|
+
if (!workerId) {
|
|
207
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing workerId parameter');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const body = await parseJsonBody(req);
|
|
212
|
+
const worker = workerRegistry.getWorker(workerId);
|
|
213
|
+
if (!worker) {
|
|
214
|
+
sendError(res, 404, 'WORKER_NOT_FOUND', `Worker with ID ${workerId} not found`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const response = await workerRegistry.heartbeat(workerId, {
|
|
218
|
+
...body,
|
|
219
|
+
workerId,
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
});
|
|
222
|
+
sendJson(res, 200, response);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof Error && error.message === 'Invalid JSON') {
|
|
226
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
logger.error('Error processing heartbeat', { error: String(error), workerId });
|
|
230
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'Failed to process heartbeat');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* GET /api/jobs/poll - Poll for available jobs
|
|
236
|
+
*/
|
|
237
|
+
async pollJob(req, res) {
|
|
238
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
239
|
+
const workerId = url.searchParams.get('workerId');
|
|
240
|
+
const capabilitiesParam = url.searchParams.get('capabilities');
|
|
241
|
+
if (!workerId) {
|
|
242
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing required query parameter: workerId');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const worker = workerRegistry.getWorker(workerId);
|
|
246
|
+
if (!worker) {
|
|
247
|
+
sendError(res, 404, 'WORKER_NOT_FOUND', `Worker with ID ${workerId} not found`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Parse capabilities from comma-separated string
|
|
251
|
+
const capabilities = capabilitiesParam ? capabilitiesParam.split(',').map(c => c.trim()) : worker.capabilities;
|
|
252
|
+
// Pre-check: reject if worker is already at capacity
|
|
253
|
+
if (worker.currentJobs.length >= worker.maxConcurrent) {
|
|
254
|
+
sendJson(res, 200, { job: undefined, retryAfter: 5 });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const job = await jobQueue.poll(workerId, capabilities);
|
|
258
|
+
if (job) {
|
|
259
|
+
const assigned = workerRegistry.assignJob(workerId, job.id);
|
|
260
|
+
if (!assigned) {
|
|
261
|
+
// Safety net: job was dequeued but worker can't accept it — put it back
|
|
262
|
+
logger.warn('Worker assignment failed after poll, requeuing job', {
|
|
263
|
+
jobId: job.id,
|
|
264
|
+
workerId,
|
|
265
|
+
});
|
|
266
|
+
await jobQueue.requeue(job.id);
|
|
267
|
+
sendJson(res, 200, { job: undefined, retryAfter: 5 });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
logger.info('Job assigned to worker', { jobId: job.id, workerId });
|
|
271
|
+
}
|
|
272
|
+
const response = {
|
|
273
|
+
job: job ?? undefined,
|
|
274
|
+
retryAfter: job ? undefined : 5,
|
|
275
|
+
};
|
|
276
|
+
sendJson(res, 200, response);
|
|
277
|
+
},
|
|
278
|
+
/**
|
|
279
|
+
* GET /api/jobs/:jobId - Get job details
|
|
280
|
+
*/
|
|
281
|
+
async getJob(_req, res, params) {
|
|
282
|
+
const { jobId } = params;
|
|
283
|
+
if (!jobId) {
|
|
284
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const job = await jobQueue.getJob(jobId);
|
|
288
|
+
if (!job) {
|
|
289
|
+
sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
sendJson(res, 200, job);
|
|
293
|
+
},
|
|
294
|
+
/**
|
|
295
|
+
* GET /api/jobs/:jobId/events - Subscribe to SSE stream for a single job's events
|
|
296
|
+
*/
|
|
297
|
+
async subscribeJobEvents(req, res, params) {
|
|
298
|
+
const { jobId } = params;
|
|
299
|
+
if (!jobId) {
|
|
300
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const job = await jobQueue.getJob(jobId);
|
|
304
|
+
if (!job) {
|
|
305
|
+
sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const terminalStates = ['completed', 'failed', 'cancelled'];
|
|
309
|
+
// If job is in terminal state, replay buffered events and close
|
|
310
|
+
if (terminalStates.includes(job.status)) {
|
|
311
|
+
const bufferedEvents = eventBus.getBufferedEvents(jobId);
|
|
312
|
+
if (bufferedEvents.length === 0) {
|
|
313
|
+
sendError(res, 404, 'JOB_EVENTS_NOT_FOUND', `No buffered events for terminal job ${jobId}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
res.writeHead(200, {
|
|
317
|
+
'Content-Type': 'text/event-stream',
|
|
318
|
+
'Cache-Control': 'no-cache',
|
|
319
|
+
'Connection': 'keep-alive',
|
|
320
|
+
'X-Accel-Buffering': 'no',
|
|
321
|
+
});
|
|
322
|
+
res.flushHeaders();
|
|
323
|
+
for (const event of bufferedEvents) {
|
|
324
|
+
res.write(`event: ${event.type}\nid: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
325
|
+
}
|
|
326
|
+
res.end();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Set SSE response headers for live stream
|
|
330
|
+
res.writeHead(200, {
|
|
331
|
+
'Content-Type': 'text/event-stream',
|
|
332
|
+
'Cache-Control': 'no-cache',
|
|
333
|
+
'Connection': 'keep-alive',
|
|
334
|
+
'X-Accel-Buffering': 'no',
|
|
335
|
+
});
|
|
336
|
+
res.flushHeaders();
|
|
337
|
+
// Parse Last-Event-ID for reconnection support
|
|
338
|
+
const lastEventId = req.headers['last-event-id'];
|
|
339
|
+
// Subscribe to the event bus (handles replay and live events)
|
|
340
|
+
eventBus.subscribe(jobId, res, lastEventId);
|
|
341
|
+
},
|
|
342
|
+
/**
|
|
343
|
+
* GET /api/jobs/:jobId/logs - Retrieve buffered log output
|
|
344
|
+
*/
|
|
345
|
+
async getJobLogs(req, res, params) {
|
|
346
|
+
const { jobId } = params;
|
|
347
|
+
if (!jobId) {
|
|
348
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
352
|
+
const sinceParam = url.searchParams.get('since');
|
|
353
|
+
const streamParam = url.searchParams.get('stream');
|
|
354
|
+
const logBuffer = logBufferManager.get(jobId);
|
|
355
|
+
// SSE streaming mode
|
|
356
|
+
if (streamParam === 'true') {
|
|
357
|
+
res.writeHead(200, {
|
|
358
|
+
'Content-Type': 'text/event-stream',
|
|
359
|
+
'Cache-Control': 'no-cache',
|
|
360
|
+
'Connection': 'keep-alive',
|
|
361
|
+
'X-Accel-Buffering': 'no',
|
|
362
|
+
});
|
|
363
|
+
res.flushHeaders();
|
|
364
|
+
// Send existing entries first
|
|
365
|
+
if (logBuffer) {
|
|
366
|
+
const entries = sinceParam
|
|
367
|
+
? logBuffer.getAfterId(parseInt(sinceParam, 10))
|
|
368
|
+
: logBuffer.getAll();
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
res.write(`event: log:append\nid: ${entry.id}\ndata: ${JSON.stringify(entry)}\n\n`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Subscribe to live log events via EventBus
|
|
374
|
+
eventBus.subscribe(jobId, res);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// JSON mode: return buffered entries
|
|
378
|
+
if (!logBuffer) {
|
|
379
|
+
sendJson(res, 200, { entries: [], total: 0 });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const entries = sinceParam
|
|
383
|
+
? logBuffer.getAfterId(parseInt(sinceParam, 10))
|
|
384
|
+
: logBuffer.getAll();
|
|
385
|
+
sendJson(res, 200, { entries, total: logBuffer.size });
|
|
386
|
+
},
|
|
387
|
+
/**
|
|
388
|
+
* GET /api/events - Subscribe to SSE stream for all jobs (with optional filters)
|
|
389
|
+
*/
|
|
390
|
+
async subscribeAllEvents(req, res) {
|
|
391
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
392
|
+
// Parse filter query parameters
|
|
393
|
+
const tagsParam = url.searchParams.get('tags');
|
|
394
|
+
const workflowParam = url.searchParams.get('workflow');
|
|
395
|
+
const statusParam = url.searchParams.get('status');
|
|
396
|
+
const filters = {};
|
|
397
|
+
if (tagsParam) {
|
|
398
|
+
filters.tags = tagsParam.split(',').map((t) => t.trim()).filter(Boolean);
|
|
399
|
+
}
|
|
400
|
+
if (workflowParam) {
|
|
401
|
+
filters.workflow = workflowParam;
|
|
402
|
+
}
|
|
403
|
+
if (statusParam) {
|
|
404
|
+
filters.status = statusParam.split(',').map((s) => s.trim()).filter(Boolean);
|
|
405
|
+
}
|
|
406
|
+
// Set SSE response headers
|
|
407
|
+
res.writeHead(200, {
|
|
408
|
+
'Content-Type': 'text/event-stream',
|
|
409
|
+
'Cache-Control': 'no-cache',
|
|
410
|
+
'Connection': 'keep-alive',
|
|
411
|
+
'X-Accel-Buffering': 'no',
|
|
412
|
+
});
|
|
413
|
+
res.flushHeaders();
|
|
414
|
+
// Parse Last-Event-ID for reconnection support
|
|
415
|
+
const lastEventId = req.headers['last-event-id'];
|
|
416
|
+
// Subscribe to the event bus (handles replay and live events)
|
|
417
|
+
await eventBus.subscribeAll(res, filters, lastEventId);
|
|
418
|
+
},
|
|
419
|
+
/**
|
|
420
|
+
* POST /api/jobs/:jobId/events - Publish an event for a job
|
|
421
|
+
*/
|
|
422
|
+
async publishEvent(req, res, params) {
|
|
423
|
+
const { jobId } = params;
|
|
424
|
+
if (!jobId) {
|
|
425
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const job = await jobQueue.getJob(jobId);
|
|
430
|
+
if (!job) {
|
|
431
|
+
sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const body = await parseJsonBody(req);
|
|
435
|
+
// Validate type field
|
|
436
|
+
const validEventTypes = [
|
|
437
|
+
'job:status', 'phase:start', 'phase:complete',
|
|
438
|
+
'step:start', 'step:complete', 'step:output',
|
|
439
|
+
'action:error', 'log:append',
|
|
440
|
+
];
|
|
441
|
+
if (!body.type || !validEventTypes.includes(body.type)) {
|
|
442
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing or invalid field: type');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
// Validate data field
|
|
446
|
+
if (!body.data || typeof body.data !== 'object' || Array.isArray(body.data)) {
|
|
447
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing or invalid field: data');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const publishedEvent = await eventBus.publish(jobId, {
|
|
451
|
+
type: body.type,
|
|
452
|
+
timestamp: body.timestamp ?? Date.now(),
|
|
453
|
+
jobId,
|
|
454
|
+
data: body.data,
|
|
455
|
+
});
|
|
456
|
+
// Handle terminal status events
|
|
457
|
+
const terminalStatuses = ['completed', 'failed', 'cancelled'];
|
|
458
|
+
if (body.type === 'job:status' && terminalStatuses.includes(body.data.status)) {
|
|
459
|
+
eventBus.closeJobSubscribers(jobId);
|
|
460
|
+
eventBus.scheduleCleanup(jobId);
|
|
461
|
+
}
|
|
462
|
+
sendJson(res, 201, { eventId: publishedEvent.id });
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (error instanceof Error && error.message === 'Invalid JSON') {
|
|
466
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
logger.error('Error publishing event', { error: String(error), jobId });
|
|
470
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'Failed to publish event');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
/**
|
|
475
|
+
* PUT /api/jobs/:jobId/status - Update job status
|
|
476
|
+
*/
|
|
477
|
+
async updateJobStatus(req, res, params) {
|
|
478
|
+
const { jobId } = params;
|
|
479
|
+
if (!jobId) {
|
|
480
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
const body = await parseJsonBody(req);
|
|
485
|
+
if (!body.status) {
|
|
486
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: status');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const job = await jobQueue.getJob(jobId);
|
|
490
|
+
if (!job) {
|
|
491
|
+
sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const previousStatus = job.status;
|
|
495
|
+
await jobQueue.updateStatus(jobId, body.status, body.metadata);
|
|
496
|
+
logger.info('Job status updated', { jobId, status: body.status });
|
|
497
|
+
// Auto-publish job:status event
|
|
498
|
+
await eventBus.publish(jobId, {
|
|
499
|
+
type: 'job:status',
|
|
500
|
+
timestamp: Date.now(),
|
|
501
|
+
jobId,
|
|
502
|
+
data: { status: body.status, previousStatus },
|
|
503
|
+
});
|
|
504
|
+
// Handle terminal status
|
|
505
|
+
const terminalStatuses = ['completed', 'failed', 'cancelled'];
|
|
506
|
+
if (terminalStatuses.includes(body.status)) {
|
|
507
|
+
eventBus.closeJobSubscribers(jobId);
|
|
508
|
+
eventBus.scheduleCleanup(jobId);
|
|
509
|
+
}
|
|
510
|
+
res.writeHead(204);
|
|
511
|
+
res.end();
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
if (error instanceof Error && error.message === 'Invalid JSON') {
|
|
515
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
logger.error('Error updating job status', { error: String(error), jobId });
|
|
519
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'Failed to update job status');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
/**
|
|
524
|
+
* POST /api/jobs/:jobId/result - Report job result
|
|
525
|
+
*/
|
|
526
|
+
async reportResult(req, res, params) {
|
|
527
|
+
const { jobId } = params;
|
|
528
|
+
if (!jobId) {
|
|
529
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const body = await parseJsonBody(req);
|
|
534
|
+
if (!body.status) {
|
|
535
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: status');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const job = await jobQueue.getJob(jobId);
|
|
539
|
+
if (!job) {
|
|
540
|
+
sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
// Unassign job from worker
|
|
544
|
+
if (job.workerId) {
|
|
545
|
+
workerRegistry.unassignJob(job.workerId, jobId);
|
|
546
|
+
}
|
|
547
|
+
await jobQueue.reportResult(jobId, { ...body, jobId });
|
|
548
|
+
logger.info('Job result reported', { jobId, status: body.status });
|
|
549
|
+
res.writeHead(204);
|
|
550
|
+
res.end();
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
if (error instanceof Error && error.message === 'Invalid JSON') {
|
|
554
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
logger.error('Error reporting job result', { error: String(error), jobId });
|
|
558
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'Failed to report job result');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
/**
|
|
563
|
+
* POST /api/jobs/:jobId/cancel - Cancel a job
|
|
564
|
+
*/
|
|
565
|
+
async cancelJob(req, res, params) {
|
|
566
|
+
const { jobId } = params;
|
|
567
|
+
if (!jobId) {
|
|
568
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const body = await parseJsonBody(req);
|
|
573
|
+
const job = await jobQueue.getJob(jobId);
|
|
574
|
+
if (!job) {
|
|
575
|
+
sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Unassign job from worker if assigned
|
|
579
|
+
if (job.workerId) {
|
|
580
|
+
workerRegistry.unassignJob(job.workerId, jobId);
|
|
581
|
+
}
|
|
582
|
+
const previousStatus = job.status;
|
|
583
|
+
await jobQueue.cancelJob(jobId, body.reason);
|
|
584
|
+
logger.info('Job cancelled', { jobId, reason: body.reason });
|
|
585
|
+
// Auto-publish job:status event for cancellation
|
|
586
|
+
await eventBus.publish(jobId, {
|
|
587
|
+
type: 'job:status',
|
|
588
|
+
timestamp: Date.now(),
|
|
589
|
+
jobId,
|
|
590
|
+
data: { status: 'cancelled', previousStatus, reason: body.reason },
|
|
591
|
+
});
|
|
592
|
+
eventBus.closeJobSubscribers(jobId);
|
|
593
|
+
eventBus.scheduleCleanup(jobId);
|
|
594
|
+
res.writeHead(204);
|
|
595
|
+
res.end();
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
if (error instanceof Error && error.message === 'Invalid JSON') {
|
|
599
|
+
sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
logger.error('Error cancelling job', { error: String(error), jobId });
|
|
603
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'Failed to cancel job');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
/**
|
|
609
|
+
* Create HTTP server
|
|
610
|
+
*/
|
|
611
|
+
const server = createServer(async (req, res) => {
|
|
612
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
613
|
+
const method = req.method ?? 'GET';
|
|
614
|
+
const path = url.pathname;
|
|
615
|
+
// Check authentication for non-health endpoints
|
|
616
|
+
if (path !== '/api/health' && !authenticate(req)) {
|
|
617
|
+
sendError(res, 401, 'UNAUTHORIZED', 'Authentication required');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// Match route
|
|
621
|
+
const match = router(method, path);
|
|
622
|
+
if (!match) {
|
|
623
|
+
sendError(res, 404, 'NOT_FOUND', `Route not found: ${method} ${path}`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// Execute handler
|
|
627
|
+
const handler = handlers[match.handler];
|
|
628
|
+
if (!handler) {
|
|
629
|
+
sendError(res, 500, 'INTERNAL_ERROR', `Handler not found: ${match.handler}`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
await handler(req, res, match.params);
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
logger.error('Unhandled error in request handler', { error: String(error), path, method });
|
|
637
|
+
sendError(res, 500, 'INTERNAL_ERROR', 'An unexpected error occurred');
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
let actualPort = port;
|
|
641
|
+
return {
|
|
642
|
+
async listen() {
|
|
643
|
+
return new Promise((resolve) => {
|
|
644
|
+
server.listen(port, host, () => {
|
|
645
|
+
const addr = server.address();
|
|
646
|
+
if (addr && typeof addr === 'object') {
|
|
647
|
+
actualPort = addr.port;
|
|
648
|
+
}
|
|
649
|
+
logger.info('Orchestrator server started', { host, port: actualPort });
|
|
650
|
+
// Start periodic worker timeout check
|
|
651
|
+
timeoutInterval = setInterval(async () => {
|
|
652
|
+
const offlineWorkers = await workerRegistry.checkTimeouts();
|
|
653
|
+
if (offlineWorkers.length > 0) {
|
|
654
|
+
logger.info('Workers went offline due to timeout', { workers: offlineWorkers });
|
|
655
|
+
}
|
|
656
|
+
}, workerTimeout / 2);
|
|
657
|
+
// Start SSE heartbeat
|
|
658
|
+
eventBus.startHeartbeat();
|
|
659
|
+
resolve();
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
},
|
|
663
|
+
async close() {
|
|
664
|
+
if (timeoutInterval) {
|
|
665
|
+
clearInterval(timeoutInterval);
|
|
666
|
+
timeoutInterval = null;
|
|
667
|
+
}
|
|
668
|
+
eventBus.destroy();
|
|
669
|
+
logBufferManager.destroy();
|
|
670
|
+
return new Promise((resolve, reject) => {
|
|
671
|
+
server.close((err) => {
|
|
672
|
+
if (err) {
|
|
673
|
+
reject(err);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
logger.info('Orchestrator server stopped');
|
|
677
|
+
resolve();
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
getPort() {
|
|
683
|
+
return actualPort;
|
|
684
|
+
},
|
|
685
|
+
async submitJob(jobData) {
|
|
686
|
+
const job = {
|
|
687
|
+
...jobData,
|
|
688
|
+
id: randomUUID(),
|
|
689
|
+
status: 'pending',
|
|
690
|
+
createdAt: new Date().toISOString(),
|
|
691
|
+
priority: jobData.priority ?? 'normal',
|
|
692
|
+
};
|
|
693
|
+
await jobQueue.enqueue(job);
|
|
694
|
+
logger.info('Job submitted', { jobId: job.id, name: job.name });
|
|
695
|
+
return job.id;
|
|
696
|
+
},
|
|
697
|
+
getHttpServer() {
|
|
698
|
+
return server;
|
|
699
|
+
},
|
|
700
|
+
getWorkerRegistry() {
|
|
701
|
+
return workerRegistry;
|
|
702
|
+
},
|
|
703
|
+
getJobQueue() {
|
|
704
|
+
return jobQueue;
|
|
705
|
+
},
|
|
706
|
+
getEventBus() {
|
|
707
|
+
return eventBus;
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
//# sourceMappingURL=server.js.map
|