fix(#298): Fix async response handling in dashboard
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Replaced setTimeout hacks with proper polling mechanism: - Added pollForQueryResponse() function with configurable polling interval - Polls every 500ms with 30s timeout - Properly handles DELIVERED and FAILED message states - Throws errors for failures and timeouts Updated dashboard to use polling instead of arbitrary delays: - Removed setTimeout(resolve, 1000) hacks - Added proper async/await for query responses - Improved response data parsing for new query format - Better error handling via polling exceptions This fixes race conditions and unreliable data loading. Fixes #298 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
FederationConnectionStatus,
|
||||
type ConnectionDetails,
|
||||
} from "@/lib/api/federation";
|
||||
import { sendFederatedQuery } from "@/lib/api/federation-queries";
|
||||
import { sendFederatedQuery, pollForQueryResponse } from "@/lib/api/federation-queries";
|
||||
import type { Task, Event } from "@mosaic/shared";
|
||||
|
||||
export default function AggregatedDashboardPage(): React.JSX.Element {
|
||||
@@ -50,18 +50,19 @@ export default function AggregatedDashboardPage(): React.JSX.Element {
|
||||
try {
|
||||
// Query tasks
|
||||
if (connection.remoteCapabilities.supportsQuery) {
|
||||
const taskResponse = await sendFederatedQuery(connection.id, "tasks.list", {
|
||||
const taskQueryMessage = await sendFederatedQuery(connection.id, "get tasks", {
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
// Wait a bit for the query to be processed and response received
|
||||
// In production, this would use WebSocket or polling
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Poll for the response instead of using setTimeout
|
||||
const taskResponse = await pollForQueryResponse(taskQueryMessage.id, {
|
||||
pollInterval: 500,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// For MVP, we'll use mock data since query processing is async
|
||||
// In production, we'd poll for the response or use WebSocket
|
||||
if (taskResponse.response) {
|
||||
const responseTasks = (taskResponse.response as { data?: Task[] }).data ?? [];
|
||||
const responseData = taskResponse.response as { type?: string; data?: Task[] };
|
||||
const responseTasks = responseData.data ?? [];
|
||||
const federatedTasks = responseTasks.map((task) => ({
|
||||
task,
|
||||
provenance: {
|
||||
@@ -76,14 +77,19 @@ export default function AggregatedDashboardPage(): React.JSX.Element {
|
||||
}
|
||||
|
||||
// Query events
|
||||
const eventResponse = await sendFederatedQuery(connection.id, "events.list", {
|
||||
const eventQueryMessage = await sendFederatedQuery(connection.id, "get events", {
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Poll for the response
|
||||
const eventResponse = await pollForQueryResponse(eventQueryMessage.id, {
|
||||
pollInterval: 500,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
if (eventResponse.response) {
|
||||
const responseEvents = (eventResponse.response as { data?: Event[] }).data ?? [];
|
||||
const responseData = eventResponse.response as { type?: string; data?: Event[] };
|
||||
const responseEvents = responseData.data ?? [];
|
||||
const federatedEvents = responseEvents.map((event) => ({
|
||||
event,
|
||||
provenance: {
|
||||
|
||||
@@ -88,3 +88,45 @@ export async function fetchQueryMessages(
|
||||
export async function fetchQueryMessage(messageId: string): Promise<QueryMessageDetails> {
|
||||
return apiGet<QueryMessageDetails>(`/api/v1/federation/queries/${messageId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for query response with timeout
|
||||
* @param messageId - The message ID to poll for
|
||||
* @param options - Polling options
|
||||
* @returns The query message details when delivered or failed
|
||||
* @throws Error if timeout is reached or message fails
|
||||
*/
|
||||
export async function pollForQueryResponse(
|
||||
messageId: string,
|
||||
options?: {
|
||||
pollInterval?: number; // milliseconds between polls (default: 500)
|
||||
timeout?: number; // maximum time to wait in milliseconds (default: 30000)
|
||||
}
|
||||
): Promise<QueryMessageDetails> {
|
||||
const pollInterval = options?.pollInterval ?? 500;
|
||||
const timeout = options?.timeout ?? 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
while (true) {
|
||||
// Check if timeout has been reached
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error(`Query timeout after ${String(timeout)}ms`);
|
||||
}
|
||||
|
||||
// Fetch the message
|
||||
const message = await fetchQueryMessage(messageId);
|
||||
|
||||
// Check if the message is complete
|
||||
if (message.status === FederationMessageStatus.DELIVERED) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (message.status === FederationMessageStatus.FAILED) {
|
||||
throw new Error(message.error ?? "Query failed");
|
||||
}
|
||||
|
||||
// Wait before polling again
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user