Portal Community

ExecutionHub — Server Side

// ExecutionHub.cs
[Authorize]
public class ExecutionHub : Hub
{
    // Client joins the execution group to receive events for one execution
    public async Task JoinExecution(string executionId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, $"exec:{executionId}");
    }

    public async Task LeaveExecution(string executionId)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"exec:{executionId}");
    }
}

// The buffer flushes by calling:
await _hubContext.Clients
    .Group($"exec:{executionId}")
    .SendAsync("NodeLogEmitted", logEntries, cancellationToken);

NodeLogEmitted Payload Schema

// The hub method sends an array of LogEntry objects (batch):
interface NodeLogEmittedPayload {
    entries: LogEntry[];  // 1..N entries per message (batched by buffer)
}

interface LogEntry {
    logId     : string;   // GUID — for deduplication
    nodeId    : string;
    level     : 'Trace' | 'Debug' | 'Information' | 'Warning' | 'Error';
    message   : string;
    timestamp : string;   // ISO 8601
    fields    : Record<string, string>;  // includes executionId, tenantId, traceId
    traceId  ?: string;
}

Client Connection

// useExecutionSignalR.ts
const connection = new HubConnectionBuilder()
    .withUrl('/hubs/execution', {
        accessTokenFactory: () => authStore.getState().accessToken
    })
    .withAutomaticReconnect()
    .build();

await connection.start();

// Join the group for this execution
await connection.invoke('JoinExecution', executionId);

// Listen for log batches
connection.on('NodeLogEmitted', (payload: NodeLogEmittedPayload) => {
    payload.entries.forEach(entry =>
        useExecutionStore.getState().appendLog(entry)
    );
});

// On unmount: leave group
return () => {
    connection.invoke('LeaveExecution', executionId);
    connection.stop();
};
Group-scoped delivery: Only clients that have joined the exec:{executionId} group receive NodeLogEmitted events for that execution. Other users watching different executions, or with no execution open, receive nothing. This is the tenant isolation boundary for real-time log streaming.