739 lines
20 KiB
TypeScript
739 lines
20 KiB
TypeScript
import { SupabaseClient } from "@supabase/supabase-js";
|
|
import {
|
|
AnalyticsDetails,
|
|
AnalyticsMetrics,
|
|
APIKeyCredentials,
|
|
Block,
|
|
CredentialsDeleteNeedConfirmationResponse,
|
|
CredentialsDeleteResponse,
|
|
CredentialsMetaResponse,
|
|
GraphExecution,
|
|
Graph,
|
|
GraphCreatable,
|
|
GraphExecuteResponse,
|
|
GraphMeta,
|
|
GraphUpdateable,
|
|
NodeExecutionResult,
|
|
MyAgentsResponse,
|
|
OAuth2Credentials,
|
|
ProfileDetails,
|
|
User,
|
|
StoreAgentsResponse,
|
|
StoreAgentDetails,
|
|
CreatorsResponse,
|
|
CreatorDetails,
|
|
StoreSubmissionsResponse,
|
|
StoreSubmissionRequest,
|
|
StoreSubmission,
|
|
StoreReviewCreate,
|
|
StoreReview,
|
|
ScheduleCreatable,
|
|
Schedule,
|
|
APIKeyPermission,
|
|
CreateAPIKeyResponse,
|
|
APIKey,
|
|
} from "./types";
|
|
import { createBrowserClient } from "@supabase/ssr";
|
|
import getServerSupabase from "../supabase/getServerSupabase";
|
|
|
|
const isClient = typeof window !== "undefined";
|
|
|
|
export default class BackendAPI {
|
|
private baseUrl: string;
|
|
private wsUrl: string;
|
|
private webSocket: WebSocket | null = null;
|
|
private wsConnecting: Promise<void> | null = null;
|
|
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
|
|
heartbeatInterval: number | null = null;
|
|
readonly HEARTBEAT_INTERVAL = 10_0000; // 100 seconds
|
|
readonly HEARTBEAT_TIMEOUT = 10_000; // 10 seconds
|
|
heartbeatTimeoutId: number | null = null;
|
|
|
|
constructor(
|
|
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
|
|
"http://localhost:8006/api",
|
|
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
|
|
"ws://localhost:8001/ws",
|
|
) {
|
|
this.baseUrl = baseUrl;
|
|
this.wsUrl = wsUrl;
|
|
}
|
|
|
|
private get supabaseClient(): SupabaseClient | null {
|
|
return isClient
|
|
? createBrowserClient(
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
)
|
|
: getServerSupabase();
|
|
}
|
|
|
|
async isAuthenticated(): Promise<boolean> {
|
|
if (!this.supabaseClient) return false;
|
|
const {
|
|
data: { user },
|
|
} = await this.supabaseClient?.auth.getUser();
|
|
return user != null;
|
|
}
|
|
|
|
createUser(): Promise<User> {
|
|
return this._request("POST", "/auth/user", {});
|
|
}
|
|
|
|
getUserCredit(page?: string): Promise<{ credits: number }> {
|
|
try {
|
|
return this._get(`/credits`, undefined, page);
|
|
} catch (error) {
|
|
return Promise.resolve({ credits: 0 });
|
|
}
|
|
}
|
|
|
|
requestTopUp(amount: number): Promise<{ checkout_url: string }> {
|
|
return this._request("POST", "/credits", { amount });
|
|
}
|
|
|
|
fulfillCheckout(): Promise<void> {
|
|
return this._request("PATCH", "/credits");
|
|
}
|
|
|
|
getBlocks(): Promise<Block[]> {
|
|
return this._get("/blocks");
|
|
}
|
|
|
|
listGraphs(): Promise<GraphMeta[]> {
|
|
return this._get(`/graphs`);
|
|
}
|
|
|
|
getExecutions(): Promise<GraphExecution[]> {
|
|
return this._get(`/executions`);
|
|
}
|
|
|
|
getGraph(
|
|
id: string,
|
|
version?: number,
|
|
hide_credentials?: boolean,
|
|
): Promise<Graph> {
|
|
let query: Record<string, any> = {};
|
|
if (version !== undefined) {
|
|
query["version"] = version;
|
|
}
|
|
if (hide_credentials !== undefined) {
|
|
query["hide_credentials"] = hide_credentials;
|
|
}
|
|
return this._get(`/graphs/${id}`, query);
|
|
}
|
|
|
|
getGraphAllVersions(id: string): Promise<Graph[]> {
|
|
return this._get(`/graphs/${id}/versions`);
|
|
}
|
|
|
|
createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
|
|
|
|
createGraph(graphID: GraphCreatable | string): Promise<Graph> {
|
|
let requestBody = { graph: graphID } as GraphCreateRequestBody;
|
|
|
|
return this._request("POST", "/graphs", requestBody);
|
|
}
|
|
|
|
updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
|
|
return this._request("PUT", `/graphs/${id}`, graph);
|
|
}
|
|
|
|
deleteGraph(id: string): Promise<void> {
|
|
return this._request("DELETE", `/graphs/${id}`);
|
|
}
|
|
|
|
setGraphActiveVersion(id: string, version: number): Promise<Graph> {
|
|
return this._request("PUT", `/graphs/${id}/versions/active`, {
|
|
active_graph_version: version,
|
|
});
|
|
}
|
|
|
|
executeGraph(
|
|
id: string,
|
|
inputData: { [key: string]: any } = {},
|
|
): Promise<GraphExecuteResponse> {
|
|
return this._request("POST", `/graphs/${id}/execute`, inputData);
|
|
}
|
|
|
|
async getGraphExecutionInfo(
|
|
graphID: string,
|
|
runID: string,
|
|
): Promise<NodeExecutionResult[]> {
|
|
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
|
|
parseNodeExecutionResultTimestamps,
|
|
);
|
|
}
|
|
|
|
async stopGraphExecution(
|
|
graphID: string,
|
|
runID: string,
|
|
): Promise<NodeExecutionResult[]> {
|
|
return (
|
|
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
|
|
).map(parseNodeExecutionResultTimestamps);
|
|
}
|
|
|
|
oAuthLogin(
|
|
provider: string,
|
|
scopes?: string[],
|
|
): Promise<{ login_url: string; state_token: string }> {
|
|
const query = scopes ? { scopes: scopes.join(",") } : undefined;
|
|
return this._get(`/integrations/${provider}/login`, query);
|
|
}
|
|
|
|
oAuthCallback(
|
|
provider: string,
|
|
code: string,
|
|
state_token: string,
|
|
): Promise<CredentialsMetaResponse> {
|
|
return this._request("POST", `/integrations/${provider}/callback`, {
|
|
code,
|
|
state_token,
|
|
});
|
|
}
|
|
|
|
createAPIKeyCredentials(
|
|
credentials: Omit<APIKeyCredentials, "id" | "type">,
|
|
): Promise<APIKeyCredentials> {
|
|
return this._request(
|
|
"POST",
|
|
`/integrations/${credentials.provider}/credentials`,
|
|
credentials,
|
|
);
|
|
}
|
|
|
|
listCredentials(provider?: string): Promise<CredentialsMetaResponse[]> {
|
|
return this._get(
|
|
provider
|
|
? `/integrations/${provider}/credentials`
|
|
: "/integrations/credentials",
|
|
);
|
|
}
|
|
|
|
getCredentials(
|
|
provider: string,
|
|
id: string,
|
|
): Promise<APIKeyCredentials | OAuth2Credentials> {
|
|
return this._get(`/integrations/${provider}/credentials/${id}`);
|
|
}
|
|
|
|
deleteCredentials(
|
|
provider: string,
|
|
id: string,
|
|
force: boolean = true,
|
|
): Promise<
|
|
CredentialsDeleteResponse | CredentialsDeleteNeedConfirmationResponse
|
|
> {
|
|
return this._request(
|
|
"DELETE",
|
|
`/integrations/${provider}/credentials/${id}`,
|
|
force ? { force: true } : undefined,
|
|
);
|
|
}
|
|
|
|
// API Key related requests
|
|
async createAPIKey(
|
|
name: string,
|
|
permissions: APIKeyPermission[],
|
|
description?: string,
|
|
): Promise<CreateAPIKeyResponse> {
|
|
return this._request("POST", "/api-keys", {
|
|
name,
|
|
permissions,
|
|
description,
|
|
});
|
|
}
|
|
|
|
async listAPIKeys(): Promise<APIKey[]> {
|
|
return this._get("/api-keys");
|
|
}
|
|
|
|
async revokeAPIKey(keyId: string): Promise<APIKey> {
|
|
return this._request("DELETE", `/api-keys/${keyId}`);
|
|
}
|
|
|
|
async updateAPIKeyPermissions(
|
|
keyId: string,
|
|
permissions: APIKeyPermission[],
|
|
): Promise<APIKey> {
|
|
return this._request("PUT", `/api-keys/${keyId}/permissions`, {
|
|
permissions,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @returns `true` if a ping event was received, `false` if provider doesn't support pinging but the webhook exists.
|
|
* @throws `Error` if the webhook does not exist.
|
|
* @throws `Error` if the attempt to ping timed out.
|
|
*/
|
|
async pingWebhook(webhook_id: string): Promise<boolean> {
|
|
return this._request("POST", `/integrations/webhooks/${webhook_id}/ping`);
|
|
}
|
|
|
|
logMetric(metric: AnalyticsMetrics) {
|
|
return this._request("POST", "/analytics/log_raw_metric", metric);
|
|
}
|
|
|
|
logAnalytic(analytic: AnalyticsDetails) {
|
|
return this._request("POST", "/analytics/log_raw_analytics", analytic);
|
|
}
|
|
|
|
///////////////////////////////////////////
|
|
/////////// V2 STORE API /////////////////
|
|
/////////////////////////////////////////
|
|
|
|
getStoreProfile(page?: string): Promise<ProfileDetails | null> {
|
|
try {
|
|
console.log("+++ Making API from: ", page);
|
|
const result = this._get("/store/profile", undefined, page);
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Error fetching store profile:", error);
|
|
return Promise.resolve(null);
|
|
}
|
|
}
|
|
|
|
getStoreAgents(params?: {
|
|
featured?: boolean;
|
|
creator?: string;
|
|
sorted_by?: string;
|
|
search_query?: string;
|
|
category?: string;
|
|
page?: number;
|
|
page_size?: number;
|
|
}): Promise<StoreAgentsResponse> {
|
|
return this._get("/store/agents", params);
|
|
}
|
|
|
|
getStoreAgent(
|
|
username: string,
|
|
agentName: string,
|
|
): Promise<StoreAgentDetails> {
|
|
return this._get(
|
|
`/store/agents/${encodeURIComponent(username)}/${encodeURIComponent(
|
|
agentName,
|
|
)}`,
|
|
);
|
|
}
|
|
|
|
getStoreCreators(params?: {
|
|
featured?: boolean;
|
|
search_query?: string;
|
|
sorted_by?: string;
|
|
page?: number;
|
|
page_size?: number;
|
|
}): Promise<CreatorsResponse> {
|
|
return this._get("/store/creators", params);
|
|
}
|
|
|
|
getStoreCreator(username: string): Promise<CreatorDetails> {
|
|
return this._get(`/store/creator/${encodeURIComponent(username)}`);
|
|
}
|
|
|
|
getStoreSubmissions(params?: {
|
|
page?: number;
|
|
page_size?: number;
|
|
}): Promise<StoreSubmissionsResponse> {
|
|
return this._get("/store/submissions", params);
|
|
}
|
|
|
|
createStoreSubmission(
|
|
submission: StoreSubmissionRequest,
|
|
): Promise<StoreSubmission> {
|
|
return this._request("POST", "/store/submissions", submission);
|
|
}
|
|
|
|
generateStoreSubmissionImage(
|
|
agent_id: string,
|
|
): Promise<{ image_url: string }> {
|
|
return this._request(
|
|
"POST",
|
|
"/store/submissions/generate_image?agent_id=" + agent_id,
|
|
);
|
|
}
|
|
|
|
deleteStoreSubmission(submission_id: string): Promise<boolean> {
|
|
return this._request("DELETE", `/store/submissions/${submission_id}`);
|
|
}
|
|
|
|
uploadStoreSubmissionMedia(file: File): Promise<string> {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
return this._uploadFile("/store/submissions/media", file);
|
|
}
|
|
|
|
updateStoreProfile(profile: ProfileDetails): Promise<ProfileDetails> {
|
|
return this._request("POST", "/store/profile", profile);
|
|
}
|
|
|
|
reviewAgent(
|
|
username: string,
|
|
agentName: string,
|
|
review: StoreReviewCreate,
|
|
): Promise<StoreReview> {
|
|
console.log("Reviewing agent: ", username, agentName, review);
|
|
return this._request(
|
|
"POST",
|
|
`/store/agents/${encodeURIComponent(username)}/${encodeURIComponent(
|
|
agentName,
|
|
)}/review`,
|
|
review,
|
|
);
|
|
}
|
|
|
|
getMyAgents(params?: {
|
|
page?: number;
|
|
page_size?: number;
|
|
}): Promise<MyAgentsResponse> {
|
|
return this._get("/store/myagents", params);
|
|
}
|
|
|
|
downloadStoreAgent(
|
|
storeListingVersionId: string,
|
|
version?: number,
|
|
): Promise<BlobPart> {
|
|
const url = version
|
|
? `/store/download/agents/${storeListingVersionId}?version=${version}`
|
|
: `/store/download/agents/${storeListingVersionId}`;
|
|
|
|
return this._get(url);
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
/////////// V2 LIBRARY API //////////////
|
|
/////////////////////////////////////////
|
|
|
|
async listLibraryAgents(): Promise<GraphMeta[]> {
|
|
return this._get("/library/agents");
|
|
}
|
|
|
|
async addAgentToLibrary(storeListingVersionId: string): Promise<void> {
|
|
await this._request("POST", `/library/agents/${storeListingVersionId}`);
|
|
}
|
|
|
|
///////////////////////////////////////////
|
|
/////////// INTERNAL FUNCTIONS ////////////
|
|
//////////////////////////////??///////////
|
|
|
|
private async _get(path: string, query?: Record<string, any>, page?: string) {
|
|
return this._request("GET", path, query, page);
|
|
}
|
|
|
|
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
|
|
return this._request("POST", `/schedules`, schedule);
|
|
}
|
|
|
|
async deleteSchedule(scheduleId: string): Promise<Schedule> {
|
|
return this._request("DELETE", `/schedules/${scheduleId}`);
|
|
}
|
|
|
|
async listSchedules(): Promise<Schedule[]> {
|
|
return this._get(`/schedules`);
|
|
}
|
|
|
|
private async _uploadFile(path: string, file: File): Promise<string> {
|
|
// Get session with retry logic
|
|
let token = "no-token-found";
|
|
let retryCount = 0;
|
|
const maxRetries = 3;
|
|
|
|
while (retryCount < maxRetries) {
|
|
const {
|
|
data: { session },
|
|
} = (await this.supabaseClient?.auth.getSession()) || {
|
|
data: { session: null },
|
|
};
|
|
|
|
if (session?.access_token) {
|
|
token = session.access_token;
|
|
break;
|
|
}
|
|
|
|
retryCount++;
|
|
if (retryCount < maxRetries) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
|
|
}
|
|
}
|
|
|
|
// Create a FormData object and append the file
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
|
|
const response = await fetch(this.baseUrl + path, {
|
|
method: "POST",
|
|
headers: {
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Error uploading file: ${response.statusText}`);
|
|
}
|
|
|
|
// Parse the response appropriately
|
|
const media_url = await response.text();
|
|
return media_url;
|
|
}
|
|
|
|
private async _request(
|
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
path: string,
|
|
payload?: Record<string, any>,
|
|
page?: string,
|
|
) {
|
|
if (method !== "GET") {
|
|
console.debug(`${method} ${path} payload:`, payload);
|
|
}
|
|
|
|
// Get session with retry logic
|
|
let token = "no-token-found";
|
|
let retryCount = 0;
|
|
const maxRetries = 3;
|
|
|
|
while (retryCount < maxRetries) {
|
|
const {
|
|
data: { session },
|
|
} = (await this.supabaseClient?.auth.getSession()) || {
|
|
data: { session: null },
|
|
};
|
|
|
|
if (session?.access_token) {
|
|
token = session.access_token;
|
|
break;
|
|
}
|
|
|
|
retryCount++;
|
|
if (retryCount < maxRetries) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
|
|
}
|
|
}
|
|
|
|
let url = this.baseUrl + path;
|
|
const payloadAsQuery = ["GET", "DELETE"].includes(method);
|
|
if (payloadAsQuery && payload) {
|
|
// For GET requests, use payload as query
|
|
const queryParams = new URLSearchParams(payload);
|
|
url += `?${queryParams.toString()}`;
|
|
}
|
|
|
|
const hasRequestBody = !payloadAsQuery && payload !== undefined;
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
...(hasRequestBody && { "Content-Type": "application/json" }),
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: hasRequestBody ? JSON.stringify(payload) : undefined,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.warn(`${method} ${path} returned non-OK response:`, response);
|
|
|
|
// console.warn("baseClient is attempting to redirect by changing window location")
|
|
// if (
|
|
// response.status === 403 &&
|
|
// response.statusText === "Not authenticated" &&
|
|
// typeof window !== "undefined" // Check if in browser environment
|
|
// ) {
|
|
// window.location.href = "/login";
|
|
// }
|
|
|
|
let errorDetail;
|
|
try {
|
|
const errorData = await response.json();
|
|
errorDetail = errorData.detail || response.statusText;
|
|
} catch (e) {
|
|
errorDetail = response.statusText;
|
|
}
|
|
|
|
throw new Error(errorDetail);
|
|
}
|
|
|
|
// Handle responses with no content (like DELETE requests)
|
|
if (
|
|
response.status === 204 ||
|
|
response.headers.get("Content-Length") === "0"
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return await response.json();
|
|
} catch (e) {
|
|
if (e instanceof SyntaxError) {
|
|
console.warn(`${method} ${path} returned invalid JSON:`, e);
|
|
return null;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
startHeartbeat() {
|
|
this.stopHeartbeat();
|
|
this.heartbeatInterval = window.setInterval(() => {
|
|
if (this.webSocket?.readyState === WebSocket.OPEN) {
|
|
this.webSocket.send(
|
|
JSON.stringify({
|
|
method: "heartbeat",
|
|
data: "ping",
|
|
success: true,
|
|
}),
|
|
);
|
|
|
|
this.heartbeatTimeoutId = window.setTimeout(() => {
|
|
console.log("Heartbeat timeout - reconnecting");
|
|
this.webSocket?.close();
|
|
this.connectWebSocket();
|
|
}, this.HEARTBEAT_TIMEOUT);
|
|
}
|
|
}, this.HEARTBEAT_INTERVAL);
|
|
}
|
|
|
|
stopHeartbeat() {
|
|
if (this.heartbeatInterval) {
|
|
clearInterval(this.heartbeatInterval);
|
|
this.heartbeatInterval = null;
|
|
}
|
|
if (this.heartbeatTimeoutId) {
|
|
clearTimeout(this.heartbeatTimeoutId);
|
|
this.heartbeatTimeoutId = null;
|
|
}
|
|
}
|
|
|
|
handleHeartbeatResponse() {
|
|
if (this.heartbeatTimeoutId) {
|
|
clearTimeout(this.heartbeatTimeoutId);
|
|
this.heartbeatTimeoutId = null;
|
|
}
|
|
}
|
|
|
|
async connectWebSocket(): Promise<void> {
|
|
this.wsConnecting ??= new Promise(async (resolve, reject) => {
|
|
try {
|
|
const token =
|
|
(await this.supabaseClient?.auth.getSession())?.data.session
|
|
?.access_token || "";
|
|
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
|
|
this.webSocket = new WebSocket(wsUrlWithToken);
|
|
|
|
this.webSocket.onopen = () => {
|
|
console.log("WebSocket connection established");
|
|
this.startHeartbeat(); // Start heartbeat when connection opens
|
|
resolve();
|
|
};
|
|
|
|
this.webSocket.onclose = (event) => {
|
|
console.log("WebSocket connection closed", event);
|
|
this.stopHeartbeat(); // Stop heartbeat when connection closes
|
|
this.webSocket = null;
|
|
// Attempt to reconnect after a delay
|
|
setTimeout(() => this.connectWebSocket(), 1000);
|
|
};
|
|
|
|
this.webSocket.onerror = (error) => {
|
|
console.error("WebSocket error:", error);
|
|
this.stopHeartbeat(); // Stop heartbeat on error
|
|
reject(error);
|
|
};
|
|
|
|
this.webSocket.onmessage = (event) => {
|
|
const message: WebsocketMessage = JSON.parse(event.data);
|
|
|
|
// Handle heartbeat response
|
|
if (message.method === "heartbeat" && message.data === "pong") {
|
|
this.handleHeartbeatResponse();
|
|
return;
|
|
}
|
|
|
|
if (message.method === "execution_event") {
|
|
message.data = parseNodeExecutionResultTimestamps(message.data);
|
|
}
|
|
this.wsMessageHandlers[message.method]?.forEach((handler) =>
|
|
handler(message.data),
|
|
);
|
|
};
|
|
} catch (error) {
|
|
console.error("Error connecting to WebSocket:", error);
|
|
reject(error);
|
|
}
|
|
});
|
|
return this.wsConnecting;
|
|
}
|
|
|
|
disconnectWebSocket() {
|
|
this.stopHeartbeat(); // Stop heartbeat when disconnecting
|
|
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
|
this.webSocket.close();
|
|
}
|
|
}
|
|
|
|
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
|
|
method: M,
|
|
data: WebsocketMessageTypeMap[M],
|
|
callCount = 0,
|
|
) {
|
|
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
|
this.webSocket.send(JSON.stringify({ method, data }));
|
|
} else {
|
|
this.connectWebSocket().then(() => {
|
|
callCount == 0
|
|
? this.sendWebSocketMessage(method, data, callCount + 1)
|
|
: setTimeout(
|
|
() => {
|
|
this.sendWebSocketMessage(method, data, callCount + 1);
|
|
},
|
|
2 ** (callCount - 1) * 1000,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
|
|
method: M,
|
|
handler: (data: WebsocketMessageTypeMap[M]) => void,
|
|
): () => void {
|
|
this.wsMessageHandlers[method] ??= new Set();
|
|
this.wsMessageHandlers[method].add(handler);
|
|
|
|
// Return detacher
|
|
return () => this.wsMessageHandlers[method].delete(handler);
|
|
}
|
|
|
|
subscribeToExecution(graphId: string) {
|
|
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
|
|
}
|
|
}
|
|
|
|
/* *** UTILITY TYPES *** */
|
|
|
|
type GraphCreateRequestBody = {
|
|
graph: GraphCreatable;
|
|
};
|
|
|
|
type WebsocketMessageTypeMap = {
|
|
subscribe: { graph_id: string };
|
|
execution_event: NodeExecutionResult;
|
|
heartbeat: "ping" | "pong";
|
|
};
|
|
|
|
type WebsocketMessage = {
|
|
[M in keyof WebsocketMessageTypeMap]: {
|
|
method: M;
|
|
data: WebsocketMessageTypeMap[M];
|
|
};
|
|
}[keyof WebsocketMessageTypeMap];
|
|
|
|
/* *** HELPER FUNCTIONS *** */
|
|
|
|
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
|
|
return {
|
|
...result,
|
|
add_time: new Date(result.add_time),
|
|
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
|
|
start_time: result.start_time ? new Date(result.start_time) : undefined,
|
|
end_time: result.end_time ? new Date(result.end_time) : undefined,
|
|
};
|
|
}
|