Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
75 changes: 52 additions & 23 deletions mobile/src/stores/WorkflowRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,25 @@ 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
| NodeUpdate
| NodeProgress
| { type: "output_update"; node_id: string; value?: unknown }
| { type: "log_update"; message?: string; content?: string }
| { type: "notification"; message?: string; content?: string }
| { type: "prediction"; node_id: string; node_name?: string }
| { type: string; message?: 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,16 +284,22 @@ 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"
const job = msg as JobUpdate;
if (state.state === "error" && job.status === "running") {return;}

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

switch (job.status) {
case "completed":
set({
Expand All @@ -287,7 +312,7 @@ function handleMessage(
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 +321,7 @@ function handleMessage(
case "running":
set({
state: "running",
statusMessage: (message.message as string) || "Running...",
statusMessage: job.message || "Running...",
});
break;
case "queued":
Expand All @@ -305,12 +330,16 @@ function handleMessage(
statusMessage: "Queued — worker is booting...",
});
break;
case "suspended":
case "suspended": {
const reason =
(message.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,7 +349,7 @@ function handleMessage(

// ── Node progress (progress/total) ─────────────────────────────
case "node_progress": {
const progress = message as unknown as NodeProgress;
const progress = msg as NodeProgress;
set({
nodeProgress: {
...state.nodeProgress,
Expand All @@ -335,8 +364,7 @@ function handleMessage(

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

const updates: Partial<WorkflowRunner> = {
Expand All @@ -347,15 +375,13 @@ function handleMessage(
statusMessage: `${update.node_name || update.node_id} ${update.status}`,
};

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

// Store per-node error
if (update.error) {
updates.nodeErrors = {
...state.nodeErrors,
Expand All @@ -379,8 +405,8 @@ function handleMessage(

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

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

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

// ── Model booting / prediction status ──────────────────────────
case "prediction": {
const nodeId = message.node_id as string;
if (nodeId) {
const pred = msg as { type: "prediction"; node_id: string; node_name?: string };
if (pred.node_id) {
set({
nodeStatus: {
...state.nodeStatus,
[nodeId]: "booting",
[pred.node_id]: "booting",
},
statusMessage: `${message.node_name || nodeId} booting...`,
statusMessage: `${pred.node_name || pred.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