Skip to content

Commit 417c51e

Browse files
committed
feat: live execution engine — BullMQ, WebSocket, node status streaming
1 parent aa697c2 commit 417c51e

6 files changed

Lines changed: 36 additions & 21 deletions

File tree

apps/api/src/executions/execution.processor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ export class ExecutionProcessor {
3131
await this.executionsService.setStatus(executionId, ExecutionStatus.RUNNING);
3232
await this.executionsService.appendLog(executionId, 'system', '▶ Starting flow execution');
3333

34+
// Give WebSocket client time to connect and join the room
35+
await new Promise((resolve) => setTimeout(resolve, 500));
36+
3437
try {
3538
const sorted = this.topologicalSort(nodes, edges);
3639
const context: Record<string, unknown> = { input: input || {} };

apps/api/src/flows/flows.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
33
import { FlowsController } from './flows.controller';
44
import { FlowsService } from './flows.service';
55
import { Flow } from './entities/flow.entity';
6+
import { ExecutionsModule } from '../executions/executions.module';
67

78
@Module({
8-
imports: [TypeOrmModule.forFeature([Flow])],
9+
imports: [TypeOrmModule.forFeature([Flow]), ExecutionsModule],
910
controllers: [FlowsController],
1011
providers: [FlowsService],
1112
exports: [FlowsService],

apps/api/src/flows/flows.service.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { Injectable, NotFoundException } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
33
import { Repository } from 'typeorm';
4-
import { Flow, FlowStatus, FlowGraph } from './entities/flow.entity';
4+
import { Flow, FlowStatus } from './entities/flow.entity';
55
import { CreateFlowDto } from './dto/create-flow.dto';
66
import { UpdateFlowDto } from './dto/update-flow.dto';
7+
import { ExecutionsService } from '../executions/executions.service';
78

89
@Injectable()
910
export class FlowsService {
1011
constructor(
1112
@InjectRepository(Flow)
1213
private readonly flowRepo: Repository<Flow>,
14+
private readonly executionsService: ExecutionsService,
1315
) {}
1416

1517
async create(workspaceId: string, dto: CreateFlowDto): Promise<Flow> {
@@ -55,9 +57,9 @@ export class FlowsService {
5557
}
5658

5759
async execute(id: string, workspaceId: string): Promise<{ id: string; status: string }> {
58-
await this.findOne(id, workspaceId);
59-
// Execution engine will be wired in next phase
60-
return { id: `exec_${Date.now()}`, status: 'queued' };
60+
const flow = await this.findOne(id, workspaceId);
61+
const execution = await this.executionsService.enqueue(flow);
62+
return { id: execution.id, status: execution.status };
6163
}
6264

6365
async remove(id: string, workspaceId: string): Promise<void> {

apps/api/src/nodes/nodes.service.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,31 @@ export class NodesService {
2020
input: Record<string, unknown>,
2121
executionId: string,
2222
): Promise<unknown> {
23-
switch (node.type) {
24-
case 'ai-llm': return this.runAiLlm(node, input);
25-
case 'web-scraper': return this.runWebScraper(node, input);
26-
case 'api-caller': return this.runApiCaller(node, input);
27-
case 'code-runner': return this.runCodeRunner(node, input);
28-
case 'email-sender': return this.runEmailSender(node, input);
23+
const nodeType = ((node.data as Record<string, unknown>)?.type as string) || node.type;
24+
switch (nodeType) {
25+
case 'ai-llm': return this.runAiLlm(node, input);
26+
case 'web-scraper': return this.runWebScraper(node, input);
27+
case 'api-caller': return this.runApiCaller(node, input);
28+
case 'code-runner': return this.runCodeRunner(node, input);
29+
case 'email-sender': return this.runEmailSender(node, input);
2930
case 'data-transform': return this.runDataTransform(node, input);
3031
case 'webhook-output': return this.runWebhookOutput(node, input);
31-
case 'condition': return this.runCondition(node, input);
32+
case 'condition': return this.runCondition(node, input);
3233
default:
33-
throw new BadRequestException(`Unknown node type: ${node.type}`);
34+
throw new BadRequestException(`Unknown node type: ${nodeType}`);
3435
}
3536
}
3637

3738
// ─── AI LLM Node ──────────────────────────────────────────
3839
private async runAiLlm(node: FlowNode, input: Record<string, unknown>): Promise<unknown> {
39-
const { prompt, model = 'gpt-4o', systemPrompt } = node.data.config as {
40+
const { prompt, model = 'gpt-4o', systemPrompt } = (node.data.config || {}) as {
4041
prompt: string;
4142
model?: string;
4243
systemPrompt?: string;
4344
};
4445

45-
// Allow prompt to reference upstream data with {{key}} syntax
46+
if (!prompt) return { text: '[no prompt configured]', usage: null };
47+
4648
const resolvedPrompt = this.interpolate(prompt, input);
4749

4850
const messages: OpenAI.ChatCompletionMessageParam[] = [];
@@ -81,13 +83,15 @@ export class NodesService {
8183

8284
// ─── API Caller Node ──────────────────────────────────────
8385
private async runApiCaller(node: FlowNode, input: Record<string, unknown>): Promise<unknown> {
84-
const { url, method = 'GET', headers = {}, body } = node.data.config as {
86+
const { url, method = 'GET', headers = {}, body } = (node.data.config || {}) as {
8587
url: string;
8688
method?: string;
8789
headers?: Record<string, string>;
8890
body?: string;
8991
};
9092

93+
if (!url) return { statusCode: 0, data: '[no url configured]', headers: {} };
94+
9195
const resolvedUrl = this.interpolate(url, input);
9296
const resolvedBody = body ? this.interpolate(body, input) : undefined;
9397

apps/web/src/app/flows/[id]/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ function FlowEditor({ flowId }: { flowId: string }) {
3333
const [showTriggers, setShowTriggers] = useState(false);
3434

3535
// 🔴 live node status updates via WebSocket
36-
useExecutionSocket(executionId);
36+
useExecutionSocket(executionId, () => {
37+
setIsRunning(false);
38+
});
3739

3840
useEffect(() => {
3941
if (flowId === 'new') return;
@@ -82,16 +84,15 @@ function FlowEditor({ flowId }: { flowId: string }) {
8284
};
8385

8486
const handleRun = async () => {
87+
if (flowId === 'new') { alert('Save the flow first!'); return; }
8588
setIsRunning(true);
8689
setShowLogs(true);
8790
setShowTriggers(false);
8891
try {
89-
if (flowId === 'new') { alert('Save the flow first!'); return; }
9092
const execution = await flowsApi.execute(flowId);
9193
setExecutionId(execution.id);
9294
} catch (err) {
9395
console.error('Run failed:', err);
94-
} finally {
9596
setIsRunning(false);
9697
}
9798
};

apps/web/src/lib/hooks/useExecutionSocket.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ let socket: Socket | null = null;
66

77
export type NodeExecutionStatus = 'idle' | 'running' | 'success' | 'error';
88

9-
export function useExecutionSocket(executionId: string | null) {
9+
export function useExecutionSocket(
10+
executionId: string | null,
11+
onConnected?: () => void,
12+
) {
1013
const { setNodes } = useReactFlow();
1114

1215
const updateNodeStatus = useCallback(
@@ -31,6 +34,7 @@ export function useExecutionSocket(executionId: string | null) {
3134

3235
socket.on('connect', () => {
3336
socket?.emit('join:execution', { executionId });
37+
onConnected?.();
3438
});
3539

3640
socket.on('execution:node:status', (payload: {
@@ -51,5 +55,5 @@ export function useExecutionSocket(executionId: string | null) {
5155
socket?.disconnect();
5256
socket = null;
5357
};
54-
}, [executionId, updateNodeStatus]);
58+
}, [executionId, updateNodeStatus, onConnected]);
5559
}

0 commit comments

Comments
 (0)