feat: add river-backed async task queue

This commit is contained in:
2026-05-12 10:11:54 +08:00
parent d69aaed444
commit 7e220b7477
30 changed files with 1342 additions and 200 deletions
+37 -2
View File
@@ -521,6 +521,7 @@ export async function createChatTask(
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/chat/completions', {
body: input,
headers: { 'X-Async': 'true' },
method: 'POST',
token,
});
@@ -598,6 +599,7 @@ export async function createImageGenerationTask(
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/images/generations', {
body: input,
headers: { 'X-Async': 'true' },
method: 'POST',
token,
});
@@ -609,6 +611,7 @@ export async function createImageEditTask(
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/images/edits', {
body: input,
headers: { 'X-Async': 'true' },
method: 'POST',
token,
});
@@ -636,6 +639,7 @@ export async function createVideoGenerationTask(
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/videos/generations', {
body: input,
headers: { 'X-Async': 'true' },
method: 'POST',
token,
});
@@ -656,6 +660,33 @@ export async function getTask(token: string, taskId: string): Promise<GatewayTas
return request<GatewayTask>(`/api/workspace/tasks/${taskId}`, { token });
}
export async function pollTaskUntilSettled(
token: string,
task: GatewayTask,
options: { intervalMs?: number; maxAttempts?: number | null; onUpdate?: (task: GatewayTask) => void } = {},
): Promise<GatewayTask> {
let detail = task;
const intervalMs = options.intervalMs ?? 1200;
const maxAttempts = options.maxAttempts ?? Number.POSITIVE_INFINITY;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
if (!taskIsPending(detail.status)) return detail;
try {
detail = await getTask(token, detail.id);
options.onUpdate?.(detail);
if (!taskIsPending(detail.status)) return detail;
} catch {
// Backend restarts or short network gaps should not turn a durable task into a failed UI run.
// Only an explicit terminal task status from the task detail endpoint settles the run.
}
await delay(intervalMs);
}
return detail;
}
export function taskIsPending(status: string) {
return status === 'queued' || status === 'running' || status === 'submitting';
}
export async function listTasks(token: string, query: WorkspaceTaskQuery): Promise<ListResponse<GatewayTask>> {
const search = new URLSearchParams({
page: String(query.page),
@@ -707,9 +738,9 @@ export async function getNetworkProxyConfig(token: string): Promise<GatewayNetwo
async function request<T>(
path: string,
options: { token?: string; auth?: boolean; method?: string; body?: unknown } = {},
options: { token?: string; auth?: boolean; method?: string; body?: unknown; headers?: Record<string, string> } = {},
): Promise<T> {
const headers: Record<string, string> = {};
const headers: Record<string, string> = { ...(options.headers ?? {}) };
if (options.auth !== false && options.token) {
headers.Authorization = `Bearer ${options.token}`;
}
@@ -731,6 +762,10 @@ async function request<T>(
return response.json() as Promise<T>;
}
function delay(ms: number) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function parseErrorMessage(body: string) {
return formatGatewayErrorDetails(parseErrorDetails(body));
}