Files
moteva/frontend/src/infrastructure/designGateway/events.ts
T
root d5f9cb366a refactor(infrastructure): modularize designGateway into focused modules
Split the monolithic designGateway.ts (1000+ lines) into organized modules:
- types.ts: type definitions and API contracts
- request.ts: HTTP request utilities
- responses.ts: response type definitions
- canvas.ts: canvas-related API calls
- projects.ts: project management operations
- agents.ts: agent/thread API operations
- generation.ts: image generation operations
- events.ts: event streaming and handlers
- canvasSnapshot.ts: snapshot management
- sharing.ts: share context and permissions
- assets.ts: asset operations
- clientIds.ts: client ID utilities
- shareContext.ts: share context helpers

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-10 16:58:21 +08:00

124 lines
3.7 KiB
TypeScript

import type { AgentMessage, ExtractNodeTextResponse, Project } from "@/domain/design";
import { authHeaders } from "@/infrastructure/authGateway";
import { createApiFailure } from "@/infrastructure/userMessages";
import type { ProjectEventHandlers, ProjectEventsOptions } from "./types";
export function projectEvents(projectId: string, handlers: ProjectEventHandlers, options: ProjectEventsOptions = {}) {
const controller = new AbortController();
let settled = false;
const readProject = (data: string) => {
const payload = JSON.parse(data) as { project: Project };
return payload.project;
};
const close = () => {
settled = true;
controller.abort();
};
const onEvent = (event: string, data: string) => {
if (event === "project") {
handlers.onProject?.(readProject(data));
return;
}
if (event === "thinking") {
handlers.onThinking?.(JSON.parse(data) as AgentMessage);
return;
}
if (event === "text_extraction") {
handlers.onTextExtraction?.(JSON.parse(data) as ExtractNodeTextResponse);
return;
}
if (event === "done") {
settled = true;
handlers.onDone?.(readProject(data));
controller.abort();
return;
}
if (event === "timeout") {
settled = true;
handlers.onTimeout?.(readProject(data));
controller.abort();
return;
}
if (event === "stream_error") {
settled = true;
const payload = data ? (JSON.parse(data) as { error?: string }) : {};
handlers.onError?.(new Error(payload.error ?? "Event stream failed"));
controller.abort();
}
};
void streamProjectEvents(projectEventsUrl(projectId, options), controller.signal, onEvent).catch((error: Error) => {
if (settled || controller.signal.aborted) return;
settled = true;
handlers.onError?.(error);
});
return close;
}
function projectEventsUrl(projectId: string, options: ProjectEventsOptions) {
if (options.eventsUrl) return options.eventsUrl;
const params = new URLSearchParams();
if (options.threadId) params.set("threadId", options.threadId);
const query = params.toString();
return `/api/projects/${projectId}/events${query ? `?${query}` : ""}`;
}
async function streamProjectEvents(url: string, signal: AbortSignal, onEvent: (event: string, data: string) => void) {
const headers = new Headers();
Object.entries(authHeaders()).forEach(([key, value]) => headers.set(key, value));
const response = await fetch(url, {
headers,
signal,
cache: "no-store",
credentials: "omit"
});
if (!response.ok) {
const payload = await response.json().catch(() => ({ error: response.statusText }));
throw createApiFailure("design", response, payload);
}
if (!response.body) {
throw new Error("Event stream failed");
}
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const parts = buffer.split(/\r?\n\r?\n/);
buffer = parts.pop() ?? "";
parts.forEach((part) => emitServerSentEvent(part, onEvent));
}
if (buffer.trim()) {
emitServerSentEvent(buffer, onEvent);
}
} finally {
reader.releaseLock();
}
}
function emitServerSentEvent(raw: string, onEvent: (event: string, data: string) => void) {
let event = "message";
const data: string[] = [];
raw.split(/\r?\n/).forEach((line) => {
if (line.startsWith("event:")) {
event = line.slice("event:".length).trim();
return;
}
if (line.startsWith("data:")) {
data.push(line.slice("data:".length).trimStart());
}
});
if (data.length > 0) {
onEvent(event, data.join("\n"));
}
}
export const eventOperations = {
projectEvents
};