d5f9cb366a
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>
124 lines
3.7 KiB
TypeScript
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
|
|
};
|