Files
stack/apps/web/src/lib/api/federation-queries.ts
Jason Woltje 9582d9a265
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(#298): Fix async response handling in dashboard
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>
2026-02-03 22:51:25 -06:00

133 lines
3.2 KiB
TypeScript

/**
* Federation Queries API Client
* Handles federated query requests to remote instances
*/
import { apiGet, apiPost } from "./client";
/**
* Federation message status (matches backend enum)
*/
export enum FederationMessageStatus {
PENDING = "PENDING",
DELIVERED = "DELIVERED",
FAILED = "FAILED",
}
/**
* Query message details
*/
export interface QueryMessageDetails {
id: string;
workspaceId: string;
connectionId: string;
messageType: string;
messageId: string;
correlationId?: string;
query?: string;
response?: unknown;
status: FederationMessageStatus;
error?: string;
createdAt: string;
updatedAt: string;
deliveredAt?: string;
}
/**
* Send query request
*/
export interface SendQueryRequest {
connectionId: string;
query: string;
context?: Record<string, unknown>;
}
/**
* Send a federated query to a remote instance
*/
export async function sendFederatedQuery(
connectionId: string,
query: string,
context?: Record<string, unknown>
): Promise<QueryMessageDetails> {
const request: SendQueryRequest = {
connectionId,
query,
};
if (context) {
request.context = context;
}
return apiPost<QueryMessageDetails>("/api/v1/federation/query", request);
}
/**
* Fetch all query messages for the workspace
*/
export async function fetchQueryMessages(
status?: FederationMessageStatus
): Promise<QueryMessageDetails[]> {
const params = new URLSearchParams();
if (status) {
params.append("status", status);
}
const queryString = params.toString();
const endpoint = queryString
? `/api/v1/federation/queries?${queryString}`
: "/api/v1/federation/queries";
return apiGet<QueryMessageDetails[]>(endpoint);
}
/**
* Fetch a single query message
*/
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));
}
}