Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mobile/src/services/WebSocketService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class WebSocketService {
* Send a message, connecting first if needed. The underlying manager queues
* the message if a reconnect is in progress rather than dropping it.
*/
async send(message: unknown, path: string = '/ws'): Promise<void> {
async send(message: Record<string, unknown>, path: string = '/ws'): Promise<void> {
await this.ensureConnection(path);

if (!this.wsManager) {
Expand Down
18 changes: 11 additions & 7 deletions mobile/src/stores/GraphEditorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import { create } from "zustand";
import { apiService, type WorkflowGraphInput } from "../services/api";
import { apiService, type WorkflowGraphInput, normalizeWorkflow } from "../services/api";
import type { NodeMetadata, Workflow } from "../types/ApiTypes";
import type {
ChainNode,
Expand Down Expand Up @@ -472,28 +472,32 @@ export const useGraphEditorStore = create<GraphEditorState>((set, get) => ({
saveWorkflow: async () => {
const { workflowId, workflowName, chain, connections } = get();
const graph = chainToGraph(chain, connections);
const graphInput: WorkflowGraphInput = {
nodes: graph.nodes,
edges: graph.edges,
};

try {
if (workflowId) {
const result = await apiService.saveWorkflow({
id: workflowId,
name: workflowName,
description: "",
graph: graph as unknown as WorkflowGraphInput,
graph: graphInput,
access: "private",
});
set({ isDirty: false });
return result as unknown as Workflow;
return normalizeWorkflow(result as Record<string, unknown>);
} else {
const result = await apiService.createWorkflow({
name: workflowName,
description: "",
graph: graph as unknown as WorkflowGraphInput,
graph: graphInput,
access: "private",
});
const newId = (result as unknown as Workflow).id;
set({ workflowId: newId, isDirty: false });
return result as unknown as Workflow;
const workflow = normalizeWorkflow(result as Record<string, unknown>);
set({ workflowId: workflow.id, isDirty: false });
return workflow;
}
} catch (err) {
console.error("Failed to save workflow:", err);
Expand Down
98 changes: 59 additions & 39 deletions mobile/src/stores/WorkflowRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,24 @@ export const createWorkflowRunnerStore = (
return store;
};

/**
* Incoming workflow message — discriminated union of all message types the
* runner handles. Covers protocol messages (JobUpdate, NodeUpdate,
* NodeProgress) plus lightweight wire-only shapes that lack dedicated types.
*/
type WorkflowMessage =
| (JobUpdate & Record<string, unknown>)
| (NodeUpdate & Record<string, unknown>)
| (NodeProgress & Record<string, unknown>)
| { type: "output_update"; node_id: string; value?: unknown; [key: string]: unknown }
| { type: "log_update"; message?: string; content?: string; [key: string]: unknown }
| { type: "notification"; message?: string; content?: string; [key: string]: unknown }
| { type: "prediction"; node_id: string; node_name?: string; [key: string]: unknown };

function isWorkflowMessage(msg: Record<string, unknown>): msg is WorkflowMessage {
return typeof msg.type === "string";
}

/**
* Central message handler — mirrors web's workflowUpdates.ts handleUpdate().
*/
Expand All @@ -265,29 +283,34 @@ function handleMessage(
get: () => WorkflowRunner,
message: Record<string, unknown>
) {
if (!isWorkflowMessage(message)) {return;}

const state = get();
const type = message.type as string;
const msg = message;

switch (type) {
switch (msg.type) {
// ── Job-level updates ──────────────────────────────────────────
case "job_update": {
const job = message as unknown as JobUpdate;
// Don't overwrite error state with stale "running"
if (state.state === "error" && job.status === "running") {return;}
if (state.state === "error" && msg.status === "running") {return;}

const errorText =
msg.error ||
(msg.error_message as string | undefined) ||
"Unknown error";

switch (job.status) {
switch (msg.status) {
case "completed":
set({
state: "completed",
results: job.result,
results: msg.result,
statusMessage: "Completed",
});
break;
case "failed":
case "timed_out":
set({
state: "error",
statusMessage: `Failed: ${message.error_message || job.error || "Unknown error"}`,
statusMessage: `Failed: ${errorText}`,
});
break;
case "cancelled":
Expand All @@ -296,7 +319,7 @@ function handleMessage(
case "running":
set({
state: "running",
statusMessage: (message.message as string) || "Running...",
statusMessage: msg.message || "Running...",
});
break;
case "queued":
Expand All @@ -305,12 +328,16 @@ function handleMessage(
statusMessage: "Queued — worker is booting...",
});
break;
case "suspended":
case "suspended": {
const reason =
(msg.suspension_reason as string | undefined) ||
"Waiting for input";
set({
state: "suspended",
statusMessage: `Suspended: ${message.suspension_reason || "Waiting for input"}`,
statusMessage: `Suspended: ${reason}`,
});
break;
}
case "paused":
set({ state: "paused", statusMessage: "Paused" });
break;
Expand All @@ -320,13 +347,12 @@ function handleMessage(

// ── Node progress (progress/total) ─────────────────────────────
case "node_progress": {
const progress = message as unknown as NodeProgress;
set({
nodeProgress: {
...state.nodeProgress,
[progress.node_id]: {
progress: progress.progress,
total: progress.total,
[msg.node_id]: {
progress: msg.progress,
total: msg.total,
},
},
});
Expand All @@ -335,41 +361,37 @@ function handleMessage(

// ── Node status, results, errors ───────────────────────────────
case "node_update": {
const update = message as unknown as NodeUpdate;
// Don't process updates after cancellation
if (state.state === "cancelled") {return;}

const updates: Partial<WorkflowRunner> = {
nodeStatus: {
...state.nodeStatus,
[update.node_id]: update.status,
[msg.node_id]: msg.status,
},
statusMessage: `${update.node_name || update.node_id} ${update.status}`,
statusMessage: `${msg.node_name || msg.node_id} ${msg.status}`,
};

// Store per-node result
if (update.result) {
if (msg.result) {
updates.nodeResults = {
...state.nodeResults,
[update.node_id]: update.result,
[msg.node_id]: msg.result,
};
}

// Store per-node error
if (update.error) {
if (msg.error) {
updates.nodeErrors = {
...state.nodeErrors,
[update.node_id]: update.error,
[msg.node_id]: msg.error,
};
updates.state = "error";
updates.logs = appendLog(
state.logs,
`Error [${update.node_name || update.node_id}]: ${update.error}`
`Error [${msg.node_name || msg.node_id}]: ${msg.error}`
);
} else {
updates.logs = appendLog(
state.logs,
`${update.node_name || update.node_id}: ${update.status}`
`${msg.node_name || msg.node_id}: ${msg.status}`
);
}

Expand All @@ -379,13 +401,11 @@ function handleMessage(

// ── Streaming output values ────────────────────────────────────
case "output_update": {
const nodeId = message.node_id as string;
const value = message.value;
if (nodeId && value !== undefined) {
if (msg.node_id && msg.value !== undefined) {
set({
nodeResults: {
...state.nodeResults,
[nodeId]: value,
[msg.node_id]: msg.value,
},
});
}
Expand All @@ -394,7 +414,7 @@ function handleMessage(

// ── Structured log entries ─────────────────────────────────────
case "log_update": {
const content = message.message as string || message.content as string;
const content = msg.message || msg.content;
if (content) {
set({ logs: appendLog(state.logs, content) });
}
Expand All @@ -403,7 +423,7 @@ function handleMessage(

// ── Notifications ──────────────────────────────────────────────
case "notification": {
const content = message.content as string || message.message as string;
const content = msg.content || msg.message;
if (content) {
set({
logs: appendLog(state.logs, `[notification] ${content}`),
Expand All @@ -414,23 +434,23 @@ function handleMessage(

// ── Model booting / prediction status ──────────────────────────
case "prediction": {
const nodeId = message.node_id as string;
if (nodeId) {
if (msg.node_id) {
set({
nodeStatus: {
...state.nodeStatus,
[nodeId]: "booting",
[msg.node_id]: "booting",
},
statusMessage: `${message.node_name || nodeId} booting...`,
statusMessage: `${msg.node_name || msg.node_id} booting...`,
});
}
break;
}

// ── Generic message with text ──────────────────────────────────
default: {
if (message.message && typeof message.message === "string") {
set({ logs: appendLog(state.logs, `[${type}] ${message.message}`) });
const generic = msg as { type: string; message?: string };
if (generic.message && typeof generic.message === "string") {
set({ logs: appendLog(state.logs, `[${generic.type}] ${generic.message}`) });
}
break;
}
Expand Down
Loading