mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge branch 'dev' into Torantulino-patch-1
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
"use client";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
import { exportAsJSONFile } from "@/lib/utils";
|
||||
@@ -41,7 +47,7 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
|
||||
// ============================ STATE =============================
|
||||
|
||||
const [graph, setGraph] = useState<Graph | null>(null);
|
||||
const [graph, setGraph] = useState<Graph | null>(null); // Graph version corresponding to LibraryAgent
|
||||
const [agent, setAgent] = useState<LibraryAgent | null>(null);
|
||||
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
@@ -60,7 +66,8 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
useState<boolean>(false);
|
||||
const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] =
|
||||
useState<GraphExecutionMeta | null>(null);
|
||||
const { state, updateState } = useOnboarding();
|
||||
const { state: onboardingState, updateState: updateOnboardingState } =
|
||||
useOnboarding();
|
||||
const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -77,34 +84,43 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
setSelectedSchedule(schedule);
|
||||
}, []);
|
||||
|
||||
const [graphVersions, setGraphVersions] = useState<Record<number, Graph>>({});
|
||||
const graphVersions = useRef<Record<number, Graph>>({});
|
||||
const loadingGraphVersions = useRef<Record<number, Promise<Graph>>>({});
|
||||
const getGraphVersion = useCallback(
|
||||
async (graphID: GraphID, version: number) => {
|
||||
if (graphVersions[version]) return graphVersions[version];
|
||||
if (version in graphVersions.current)
|
||||
return graphVersions.current[version];
|
||||
if (version in loadingGraphVersions.current)
|
||||
return loadingGraphVersions.current[version];
|
||||
|
||||
const graphVersion = await api.getGraph(graphID, version);
|
||||
setGraphVersions((prev) => ({
|
||||
...prev,
|
||||
[version]: graphVersion,
|
||||
}));
|
||||
return graphVersion;
|
||||
const pendingGraph = api.getGraph(graphID, version).then((graph) => {
|
||||
graphVersions.current[version] = graph;
|
||||
return graph;
|
||||
});
|
||||
// Cache promise as well to avoid duplicate requests
|
||||
loadingGraphVersions.current[version] = pendingGraph;
|
||||
return pendingGraph;
|
||||
},
|
||||
[api, graphVersions],
|
||||
[api, graphVersions, loadingGraphVersions],
|
||||
);
|
||||
|
||||
// Reward user for viewing results of their onboarding agent
|
||||
useEffect(() => {
|
||||
if (!state || !selectedRun || state.completedSteps.includes("GET_RESULTS"))
|
||||
if (
|
||||
!onboardingState ||
|
||||
!selectedRun ||
|
||||
onboardingState.completedSteps.includes("GET_RESULTS")
|
||||
)
|
||||
return;
|
||||
|
||||
if (selectedRun.id === state.onboardingAgentExecutionId) {
|
||||
updateState({
|
||||
completedSteps: [...state.completedSteps, "GET_RESULTS"],
|
||||
if (selectedRun.id === onboardingState.onboardingAgentExecutionId) {
|
||||
updateOnboardingState({
|
||||
completedSteps: [...onboardingState.completedSteps, "GET_RESULTS"],
|
||||
});
|
||||
}
|
||||
}, [selectedRun, state]);
|
||||
}, [selectedRun, onboardingState, updateOnboardingState]);
|
||||
|
||||
const fetchAgents = useCallback(() => {
|
||||
const refreshPageData = useCallback(() => {
|
||||
api.getLibraryAgent(agentID).then((agent) => {
|
||||
setAgent(agent);
|
||||
|
||||
@@ -119,38 +135,40 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
new Set(agentRuns.map((run) => run.graph_version)).forEach((version) =>
|
||||
getGraphVersion(agent.graph_id, version),
|
||||
);
|
||||
|
||||
if (!selectedView.id && isFirstLoad && agentRuns.length > 0) {
|
||||
// only for first load or first execution
|
||||
setIsFirstLoad(false);
|
||||
|
||||
const latestRun = agentRuns.reduce((latest, current) => {
|
||||
if (latest.started_at && !current.started_at) return current;
|
||||
else if (!latest.started_at) return latest;
|
||||
return latest.started_at > current.started_at ? latest : current;
|
||||
}, agentRuns[0]);
|
||||
selectView({ type: "run", id: latestRun.id });
|
||||
}
|
||||
});
|
||||
});
|
||||
if (selectedView.type == "run" && selectedView.id && agent) {
|
||||
api
|
||||
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
|
||||
.then(setSelectedRun);
|
||||
}
|
||||
}, [api, agentID, getGraphVersion, graph, selectedView, isFirstLoad, agent]);
|
||||
}, [api, agentID, getGraphVersion, graph]);
|
||||
|
||||
// On first load: select the latest run
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
// Only for first load or first execution
|
||||
if (selectedView.id || !isFirstLoad || agentRuns.length == 0) return;
|
||||
setIsFirstLoad(false);
|
||||
|
||||
const latestRun = agentRuns.reduce((latest, current) => {
|
||||
if (latest.started_at && !current.started_at) return current;
|
||||
else if (!latest.started_at) return latest;
|
||||
return latest.started_at > current.started_at ? latest : current;
|
||||
}, agentRuns[0]);
|
||||
selectView({ type: "run", id: latestRun.id });
|
||||
}, [agentRuns, isFirstLoad, selectedView.id, selectView]);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
refreshPageData();
|
||||
}, []);
|
||||
|
||||
// Subscribe to websocket updates for agent runs
|
||||
// Subscribe to WebSocket updates for agent runs
|
||||
useEffect(() => {
|
||||
if (!agent) return;
|
||||
if (!agent?.graph_id) return;
|
||||
|
||||
// Subscribe to all executions for this agent
|
||||
api.subscribeToGraphExecutions(agent.graph_id);
|
||||
}, [api, agent]);
|
||||
return api.onWebSocketConnect(() => {
|
||||
refreshPageData(); // Sync up on (re)connect
|
||||
|
||||
// Subscribe to all executions for this agent
|
||||
api.subscribeToGraphExecutions(agent.graph_id);
|
||||
});
|
||||
}, [api, agent?.graph_id, refreshPageData]);
|
||||
|
||||
// Handle execution updates
|
||||
useEffect(() => {
|
||||
@@ -179,24 +197,29 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
};
|
||||
}, [api, agent?.graph_id, selectedView.id]);
|
||||
|
||||
// load selectedRun based on selectedView
|
||||
// Pre-load selectedRun based on selectedView
|
||||
useEffect(() => {
|
||||
if (selectedView.type != "run" || !selectedView.id || !agent) return;
|
||||
if (selectedView.type != "run" || !selectedView.id) return;
|
||||
|
||||
const newSelectedRun = agentRuns.find((run) => run.id == selectedView.id);
|
||||
if (selectedView.id !== selectedRun?.id) {
|
||||
// Pull partial data from "cache" while waiting for the rest to load
|
||||
setSelectedRun(newSelectedRun ?? null);
|
||||
|
||||
// Ensure corresponding graph version is available before rendering I/O
|
||||
api
|
||||
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
|
||||
.then(async (run) => {
|
||||
await getGraphVersion(run.graph_id, run.graph_version);
|
||||
setSelectedRun(run);
|
||||
});
|
||||
}
|
||||
}, [api, selectedView, agent, agentRuns, selectedRun?.id, getGraphVersion]);
|
||||
}, [api, selectedView, agentRuns, selectedRun?.id]);
|
||||
|
||||
// Load selectedRun based on selectedView; refresh on agent refresh
|
||||
useEffect(() => {
|
||||
if (selectedView.type != "run" || !selectedView.id || !agent) return;
|
||||
|
||||
api
|
||||
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
|
||||
.then(async (run) => {
|
||||
// Ensure corresponding graph version is available before rendering I/O
|
||||
await getGraphVersion(run.graph_id, run.graph_version);
|
||||
setSelectedRun(run);
|
||||
});
|
||||
}, [api, selectedView, agent, getGraphVersion]);
|
||||
|
||||
const fetchSchedules = useCallback(async () => {
|
||||
if (!agent) return;
|
||||
@@ -328,7 +351,7 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
selectedRun && (
|
||||
<AgentRunDetailsView
|
||||
agent={agent}
|
||||
graph={graphVersions[selectedRun.graph_version] ?? graph}
|
||||
graph={graphVersions.current[selectedRun.graph_version] ?? graph}
|
||||
run={selectedRun}
|
||||
agentActions={agentActions}
|
||||
onRun={(runID) => selectRun(runID)}
|
||||
|
||||
@@ -176,6 +176,7 @@ export default function LoginPage() {
|
||||
</AuthButton>
|
||||
</form>
|
||||
<AuthFeedback
|
||||
type="login"
|
||||
message={feedback}
|
||||
isError={!!feedback}
|
||||
behaveAs={getBehaveAs()}
|
||||
|
||||
@@ -200,6 +200,7 @@ export default function ResetPasswordPage() {
|
||||
Update password
|
||||
</AuthButton>
|
||||
<AuthFeedback
|
||||
type="login"
|
||||
message={feedback}
|
||||
isError={isError}
|
||||
behaveAs={getBehaveAs()}
|
||||
@@ -242,6 +243,7 @@ export default function ResetPasswordPage() {
|
||||
Send reset email
|
||||
</AuthButton>
|
||||
<AuthFeedback
|
||||
type="login"
|
||||
message={feedback}
|
||||
isError={isError}
|
||||
behaveAs={getBehaveAs()}
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function SignupPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard className="mx-auto">
|
||||
<AuthCard className="mx-auto mt-12">
|
||||
<AuthHeader>Create a new account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSignup)}>
|
||||
@@ -215,6 +215,7 @@ export default function SignupPage() {
|
||||
</form>
|
||||
</Form>
|
||||
<AuthFeedback
|
||||
type="signup"
|
||||
message={feedback}
|
||||
isError={!!feedback}
|
||||
behaveAs={getBehaveAs()}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { AlertCircle, CheckCircle } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { HelpItem } from "@/components/auth/help-item";
|
||||
import Link from "next/link";
|
||||
import { BehaveAs } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
type: "login" | "signup";
|
||||
message?: string | null;
|
||||
isError?: boolean;
|
||||
behaveAs?: BehaveAs;
|
||||
}
|
||||
|
||||
export default function AuthFeedback({
|
||||
type,
|
||||
message = "",
|
||||
isError = false,
|
||||
behaveAs = BehaveAs.CLOUD,
|
||||
@@ -39,48 +40,45 @@ export default function AuthFeedback({
|
||||
)}
|
||||
|
||||
{/* Cloud-specific help */}
|
||||
{isError && behaveAs === BehaveAs.CLOUD && (
|
||||
<div className="mt-2 space-y-2 text-sm">
|
||||
<span className="block text-center font-medium text-red-500">
|
||||
The provided email may not be allowed to sign up.
|
||||
</span>
|
||||
<ul className="space-y-2 text-slate-700">
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2">-</span>
|
||||
<span>
|
||||
AutoGPT Platform is currently in closed beta. You can join{" "}
|
||||
<Link
|
||||
{isError &&
|
||||
behaveAs === BehaveAs.CLOUD &&
|
||||
(type === "signup" ? (
|
||||
<Card className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y divide-slate-100">
|
||||
<span className="my-3 block text-center text-sm font-medium text-red-500">
|
||||
The provided email may not be allowed to sign up.
|
||||
</span>
|
||||
<HelpItem
|
||||
title="AutoGPT Platform is currently in closed beta. "
|
||||
description="You can join "
|
||||
linkText="the waitlist here"
|
||||
href="https://agpt.co/waitlist"
|
||||
className="font-medium text-slate-950 underline hover:text-slate-700"
|
||||
>
|
||||
the waitlist here
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2">-</span>
|
||||
<span>
|
||||
Make sure you use the same email address you used to sign up for
|
||||
the waitlist.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2">-</span>
|
||||
<span>
|
||||
You can self host the platform, visit our{" "}
|
||||
<Link
|
||||
/>
|
||||
<HelpItem title="Make sure you use the same email address you used to sign up for the waitlist." />
|
||||
<HelpItem
|
||||
title="You can self host the platform!"
|
||||
description="Visit our"
|
||||
linkText="GitHub repository"
|
||||
href="https://github.com/Significant-Gravitas/AutoGPT"
|
||||
className="font-medium text-slate-950 underline hover:text-slate-700"
|
||||
>
|
||||
GitHub repository
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y divide-slate-100">
|
||||
<HelpItem
|
||||
title="Having trouble logging in?"
|
||||
description="Make sure you've already "
|
||||
linkText="signed up"
|
||||
href="/signup"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Local-specific help */}
|
||||
{isError && behaveAs === BehaveAs.LOCAL && (
|
||||
|
||||
@@ -3,29 +3,33 @@ import Link from "next/link";
|
||||
|
||||
interface HelpItemProps {
|
||||
title: string;
|
||||
description: string;
|
||||
linkText: string;
|
||||
href: string;
|
||||
description?: string;
|
||||
linkText?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function HelpItem({
|
||||
title,
|
||||
description,
|
||||
linkText,
|
||||
href,
|
||||
href = "",
|
||||
}: HelpItemProps) {
|
||||
const external = !href.startsWith("/");
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h3 className="mb-1 text-sm font-medium text-slate-950">{title}</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
{description}{" "}
|
||||
<Link
|
||||
href={href}
|
||||
className="inline-flex items-center font-medium text-slate-950 hover:text-slate-700"
|
||||
>
|
||||
{linkText}
|
||||
<ExternalLink className="ml-1 h-3 w-3" />
|
||||
</Link>
|
||||
{linkText && (
|
||||
<Link
|
||||
href={href}
|
||||
className="inline-flex items-center font-medium text-slate-950 hover:text-slate-700"
|
||||
>
|
||||
{linkText}
|
||||
{external && <ExternalLink className="ml-1 h-3 w-3" />}
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -110,28 +110,50 @@ export default function useAgentGraph(
|
||||
|
||||
// Subscribe to execution events
|
||||
useEffect(() => {
|
||||
api.onWebSocketMessage("node_execution_event", (data) => {
|
||||
if (data.graph_exec_id != flowExecutionID) {
|
||||
return;
|
||||
}
|
||||
setUpdateQueue((prev) => [...prev, data]);
|
||||
});
|
||||
const deregisterMessageHandler = api.onWebSocketMessage(
|
||||
"node_execution_event",
|
||||
(data) => {
|
||||
if (data.graph_exec_id != flowExecutionID) {
|
||||
return;
|
||||
}
|
||||
setUpdateQueue((prev) => [...prev, data]);
|
||||
},
|
||||
);
|
||||
|
||||
if (flowExecutionID) {
|
||||
api
|
||||
.subscribeToGraphExecution(flowExecutionID)
|
||||
.then(() =>
|
||||
console.debug(
|
||||
`Subscribed to updates for execution #${flowExecutionID}`,
|
||||
),
|
||||
)
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
`Failed to subscribe to updates for execution #${flowExecutionID}:`,
|
||||
error,
|
||||
),
|
||||
);
|
||||
}
|
||||
const deregisterConnectHandler =
|
||||
flowID && flowExecutionID
|
||||
? api.onWebSocketConnect(() => {
|
||||
// Subscribe to execution updates
|
||||
api
|
||||
.subscribeToGraphExecution(flowExecutionID)
|
||||
.then(() =>
|
||||
console.debug(
|
||||
`Subscribed to updates for execution #${flowExecutionID}`,
|
||||
),
|
||||
)
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
`Failed to subscribe to updates for execution #${flowExecutionID}:`,
|
||||
error,
|
||||
),
|
||||
);
|
||||
|
||||
// Sync execution info to ensure it's up-to-date after (re)connect
|
||||
api
|
||||
.getGraphExecutionInfo(flowID, flowExecutionID)
|
||||
.then((execution) =>
|
||||
setUpdateQueue((prev) => {
|
||||
if (!execution.node_executions) return prev;
|
||||
return [...prev, ...execution.node_executions];
|
||||
}),
|
||||
);
|
||||
})
|
||||
: () => {};
|
||||
|
||||
return () => {
|
||||
deregisterMessageHandler();
|
||||
deregisterConnectHandler();
|
||||
};
|
||||
}, [api, flowID, flowVersion, flowExecutionID]);
|
||||
|
||||
const getOutputType = useCallback(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import {
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type {
|
||||
AddUserCreditsResponse,
|
||||
AnalyticsDetails,
|
||||
AnalyticsMetrics,
|
||||
APIKey,
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
Credentials,
|
||||
CredentialsDeleteNeedConfirmationResponse,
|
||||
CredentialsDeleteResponse,
|
||||
CredentialsMetaInput,
|
||||
CredentialsMetaResponse,
|
||||
Graph,
|
||||
GraphCreatable,
|
||||
@@ -32,8 +36,11 @@ import {
|
||||
NodeExecutionResult,
|
||||
NotificationPreference,
|
||||
NotificationPreferenceDTO,
|
||||
OttoQuery,
|
||||
OttoResponse,
|
||||
ProfileDetails,
|
||||
RefundRequest,
|
||||
ReviewSubmissionRequest,
|
||||
Schedule,
|
||||
ScheduleCreatable,
|
||||
ScheduleID,
|
||||
@@ -45,20 +52,13 @@ import {
|
||||
StoreSubmission,
|
||||
StoreSubmissionRequest,
|
||||
StoreSubmissionsResponse,
|
||||
SubmissionStatus,
|
||||
TransactionHistory,
|
||||
User,
|
||||
UserPasswordCredentials,
|
||||
OttoQuery,
|
||||
OttoResponse,
|
||||
UserOnboarding,
|
||||
ReviewSubmissionRequest,
|
||||
SubmissionStatus,
|
||||
AddUserCreditsResponse,
|
||||
UserPasswordCredentials,
|
||||
UsersBalanceHistoryResponse,
|
||||
CredentialsMetaInput,
|
||||
} from "./types";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import getServerSupabase from "../supabase/getServerSupabase";
|
||||
|
||||
const isClient = typeof window !== "undefined";
|
||||
|
||||
@@ -67,11 +67,13 @@ export default class BackendAPI {
|
||||
private wsUrl: string;
|
||||
private webSocket: WebSocket | null = null;
|
||||
private wsConnecting: Promise<void> | null = null;
|
||||
private wsOnConnectHandlers: Set<() => void> = new Set();
|
||||
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
|
||||
heartbeatInterval: number | null = null;
|
||||
readonly HEARTBEAT_INTERVAL = 10_0000; // 100 seconds
|
||||
|
||||
readonly HEARTBEAT_INTERVAL = 100_000; // 100 seconds
|
||||
readonly HEARTBEAT_TIMEOUT = 10_000; // 10 seconds
|
||||
heartbeatTimeoutId: number | null = null;
|
||||
heartbeatIntervalID: number | null = null;
|
||||
heartbeatTimeoutID: number | null = null;
|
||||
|
||||
constructor(
|
||||
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
|
||||
@@ -699,6 +701,14 @@ export default class BackendAPI {
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
////////////// OTTO //////////////
|
||||
//////////////////////////////////
|
||||
|
||||
async askOtto(query: OttoQuery): Promise<OttoResponse> {
|
||||
return this._request("POST", "/otto/ask", query);
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
////////// INTERNAL FUNCTIONS //////////
|
||||
////////////////////////////////////////
|
||||
@@ -707,10 +717,6 @@ export default class BackendAPI {
|
||||
return this._request("GET", path, query);
|
||||
}
|
||||
|
||||
async askOtto(query: OttoQuery): Promise<OttoResponse> {
|
||||
return this._request("POST", "/otto/ask", query);
|
||||
}
|
||||
|
||||
private async _uploadFile(path: string, file: File): Promise<string> {
|
||||
// Get session with retry logic
|
||||
let token = "no-token-found";
|
||||
@@ -914,6 +920,25 @@ export default class BackendAPI {
|
||||
return () => this.wsMessageHandlers[method].delete(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* All handlers are invoked when the WebSocket (re)connects. If it's already connected
|
||||
* when this function is called, the passed handler is invoked immediately.
|
||||
*
|
||||
* Use this hook to subscribe to topics and refresh state,
|
||||
* to ensure re-subscription and re-sync on re-connect.
|
||||
*
|
||||
* @returns a detacher for the passed handler.
|
||||
*/
|
||||
onWebSocketConnect(handler: () => void): () => void {
|
||||
this.wsOnConnectHandlers.add(handler);
|
||||
|
||||
this.connectWebSocket();
|
||||
if (this.webSocket?.readyState == WebSocket.OPEN) handler();
|
||||
|
||||
// Return detacher
|
||||
return () => this.wsOnConnectHandlers.delete(handler);
|
||||
}
|
||||
|
||||
async connectWebSocket(): Promise<void> {
|
||||
this.wsConnecting ??= new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
@@ -925,6 +950,7 @@ export default class BackendAPI {
|
||||
|
||||
this.webSocket.onopen = () => {
|
||||
this._startWSHeartbeat(); // Start heartbeat when connection opens
|
||||
this.wsOnConnectHandlers.forEach((handler) => handler());
|
||||
resolve();
|
||||
};
|
||||
|
||||
@@ -943,24 +969,7 @@ export default class BackendAPI {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
this.webSocket.onmessage = (event) => {
|
||||
const message: WebsocketMessage = JSON.parse(event.data);
|
||||
|
||||
// Handle heartbeat response
|
||||
if (message.method === "heartbeat" && message.data === "pong") {
|
||||
this._handleWSHeartbeatResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "node_execution_event") {
|
||||
message.data = parseNodeExecutionResultTimestamps(message.data);
|
||||
} else if (message.method == "graph_execution_event") {
|
||||
message.data = parseGraphExecutionTimestamps(message.data);
|
||||
}
|
||||
this.wsMessageHandlers[message.method]?.forEach((handler) =>
|
||||
handler(message.data),
|
||||
);
|
||||
};
|
||||
this.webSocket.onmessage = (event) => this._handleWSMessage(event);
|
||||
} catch (error) {
|
||||
console.error("Error connecting to WebSocket:", error);
|
||||
reject(error);
|
||||
@@ -976,9 +985,28 @@ export default class BackendAPI {
|
||||
}
|
||||
}
|
||||
|
||||
_startWSHeartbeat() {
|
||||
private _handleWSMessage(event: MessageEvent): void {
|
||||
const message: WebsocketMessage = JSON.parse(event.data);
|
||||
|
||||
// Handle heartbeat response
|
||||
if (message.method === "heartbeat" && message.data === "pong") {
|
||||
this._handleWSHeartbeatResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "node_execution_event") {
|
||||
message.data = parseNodeExecutionResultTimestamps(message.data);
|
||||
} else if (message.method == "graph_execution_event") {
|
||||
message.data = parseGraphExecutionTimestamps(message.data);
|
||||
}
|
||||
this.wsMessageHandlers[message.method]?.forEach((handler) =>
|
||||
handler(message.data),
|
||||
);
|
||||
}
|
||||
|
||||
private _startWSHeartbeat() {
|
||||
this._stopWSHeartbeat();
|
||||
this.heartbeatInterval = window.setInterval(() => {
|
||||
this.heartbeatIntervalID = window.setInterval(() => {
|
||||
if (this.webSocket?.readyState === WebSocket.OPEN) {
|
||||
this.webSocket.send(
|
||||
JSON.stringify({
|
||||
@@ -988,7 +1016,7 @@ export default class BackendAPI {
|
||||
}),
|
||||
);
|
||||
|
||||
this.heartbeatTimeoutId = window.setTimeout(() => {
|
||||
this.heartbeatTimeoutID = window.setTimeout(() => {
|
||||
console.warn("Heartbeat timeout - reconnecting");
|
||||
this.webSocket?.close();
|
||||
this.connectWebSocket();
|
||||
@@ -997,21 +1025,21 @@ export default class BackendAPI {
|
||||
}, this.HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
_stopWSHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
private _stopWSHeartbeat() {
|
||||
if (this.heartbeatIntervalID) {
|
||||
clearInterval(this.heartbeatIntervalID);
|
||||
this.heartbeatIntervalID = null;
|
||||
}
|
||||
if (this.heartbeatTimeoutId) {
|
||||
clearTimeout(this.heartbeatTimeoutId);
|
||||
this.heartbeatTimeoutId = null;
|
||||
if (this.heartbeatTimeoutID) {
|
||||
clearTimeout(this.heartbeatTimeoutID);
|
||||
this.heartbeatTimeoutID = null;
|
||||
}
|
||||
}
|
||||
|
||||
_handleWSHeartbeatResponse() {
|
||||
if (this.heartbeatTimeoutId) {
|
||||
clearTimeout(this.heartbeatTimeoutId);
|
||||
this.heartbeatTimeoutId = null;
|
||||
private _handleWSHeartbeatResponse() {
|
||||
if (this.heartbeatTimeoutID) {
|
||||
clearTimeout(this.heartbeatTimeoutID);
|
||||
this.heartbeatTimeoutID = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user