Skip to content

Commit 8441aec

Browse files
committed
fix: pass node props through graph execution pipeline
- tick_execution SQL now includes 'props' in job payload alongside 'inputs' - compute-worker converts props array [{name,type,value}] to flat object for impl() - const/string, const/number, const/boolean now work end-to-end in graph execution - graph node failures no longer retry (return instead of re-throw after failGraphExecution) - add Export/Import JSON graph buttons to FlowsPanel toolbar for debugging
1 parent 52ffd34 commit 8441aec

4 files changed

Lines changed: 64 additions & 5 deletions

File tree

job/compute-worker/src/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export { TtlCache } from './cache';
3535
export type { ComputeLogEntry } from './compute-log';
3636
export { ComputeLogTracker } from './compute-log';
3737
export { FunctionDiscovery } from './discovery';
38-
export { executeInline, getInlineImpl, listInlineNodes } from './inline';
3938
export type { InlineImplFn, InlineNodeDef } from './inline';
39+
export { executeInline, getInlineImpl, listInlineNodes } from './inline';
4040
export { InvocationTracker } from './invocation';
4141
export { ComputeModuleLoader } from './module-loader';
4242
export type { ComputeRequestOptions, ComputeRequestResult } from './req';
@@ -330,6 +330,15 @@ export default class ComputeWorker {
330330
// Inline nodes use inputs directly; props come from the graph node definition
331331
const inputs = graphNode ? (payload.inputs as Record<string, unknown>) : (payload as Record<string, unknown>);
332332

333+
// Convert props from array format [{name, type, value}, ...] to flat {name: value}
334+
const propsArray = (graphNode && Array.isArray(payload.props)) ? payload.props : [];
335+
const props: Record<string, unknown> = {};
336+
for (const p of propsArray) {
337+
if (p && typeof p === 'object' && 'name' in p) {
338+
props[p.name as string] = p.value;
339+
}
340+
}
341+
333342
await this.setJobGUCs(job);
334343

335344
const { id: invocationId } = await this.tracker.create({
@@ -343,7 +352,7 @@ export default class ComputeWorker {
343352

344353
const reqStart = process.hrtime();
345354
try {
346-
const result = await executeInline(functionName, inputs, {});
355+
const result = await executeInline(functionName, inputs, props);
347356

348357
const elapsed = process.hrtime(reqStart);
349358
const ms = Math.round((elapsed[0] * 1e9 + elapsed[1]) / 1e6);
@@ -400,6 +409,9 @@ export default class ComputeWorker {
400409
} catch (graphErr) {
401410
log.error('Failed to mark graph execution as failed', graphErr);
402411
}
412+
// Don't re-throw for graph nodes — execution is already marked failed.
413+
// Re-throwing would cause the job queue to retry with the same permanent error.
414+
return;
403415
}
404416
throw err;
405417
}
@@ -538,6 +550,9 @@ export default class ComputeWorker {
538550
} catch (graphErr) {
539551
log.error('Failed to mark graph execution as failed', graphErr);
540552
}
553+
// Don't re-throw for graph nodes — execution is already marked failed.
554+
// Re-throwing would cause the job queue to retry with the same permanent error.
555+
return;
541556
}
542557
throw err;
543558
}

pgpm/constructive-compute/deploy/schemas/constructive_compute_private/procedures/platform_tick_execution/procedure.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ BEGIN
178178
payload
179179
)
180180
VALUES
181-
(v_exec.database_id, (('fbp:eval:' || v_graph.context) || ':') || v_node_type, (json_build_object('execution_id', v_exec.id, 'node_name', v_node_name, 'node_type', v_node_type, 'inputs', v_inputs))::json);
181+
(v_exec.database_id, (('fbp:eval:' || v_graph.context) || ':') || v_node_type, (json_build_object('execution_id', v_exec.id, 'node_name', v_node_name, 'node_type', v_node_type, 'inputs', v_inputs, 'props', v_node->'props'))::json);
182182
v_jobs_enqueued := v_jobs_enqueued + 1;
183183
END LOOP;
184184
UPDATE "constructive_compute_private".platform_function_graph_executions SET

pgpm/constructive-compute/sql/constructive-compute--0.0.1.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ BEGIN
389389
payload
390390
)
391391
VALUES
392-
(v_exec.database_id, v_node_type, (json_build_object('execution_id', v_exec.id, 'node_name', v_node_name, 'node_type', v_node_type, 'inputs', v_inputs))::json);
392+
(v_exec.database_id, v_node_type, (json_build_object('execution_id', v_exec.id, 'node_name', v_node_name, 'node_type', v_node_type, 'inputs', v_inputs, 'props', v_node->'props'))::json);
393393
v_jobs_enqueued := v_jobs_enqueued + 1;
394394
END LOOP;
395395
UPDATE "constructive_compute_private".platform_function_graph_executions SET

www/src/components/FlowsPanel.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import type { NodeDefinitionWithImpl } from '@fbp/evaluator';
1414
import type { Graph, NodeDefinition, Node } from '@fbp/types';
1515
import { compute } from '@constructive-functions/constructive-functions-hooks';
16-
import { RefreshCw, Save, Trash2, Plus, Play, Zap, ChevronDown, ChevronRight, Cloud, Server } from 'lucide-react';
16+
import { RefreshCw, Save, Trash2, Plus, Play, Zap, ChevronDown, ChevronRight, Cloud, Server, Download, Upload } from 'lucide-react';
1717

1818
const DATABASE_ID = '00000000-0000-0000-0000-000000000000';
1919
const BOUNDARY_NAMES = ['graph/input', 'graph/output', 'graph/prop'];
@@ -908,6 +908,36 @@ export function FlowsPanel() {
908908
e.dataTransfer.effectAllowed = 'copy';
909909
}, []);
910910

911+
const handleExportGraph = useCallback(() => {
912+
const graph = graphRef.current;
913+
const blob = new Blob([JSON.stringify(graph, null, 2)], { type: 'application/json' });
914+
const url = URL.createObjectURL(blob);
915+
const a = document.createElement('a');
916+
a.href = url;
917+
a.download = `${flowName.trim() || 'graph'}.json`;
918+
a.click();
919+
URL.revokeObjectURL(url);
920+
}, [flowName]);
921+
922+
const handleImportGraph = useCallback(() => {
923+
const input = document.createElement('input');
924+
input.type = 'file';
925+
input.accept = '.json';
926+
input.onchange = async (e) => {
927+
const file = (e.target as HTMLInputElement).files?.[0];
928+
if (!file) return;
929+
try {
930+
const text = await file.text();
931+
const graph = JSON.parse(text) as Graph;
932+
setCurrentGraph(graph);
933+
if (graph.name) setFlowName(graph.name);
934+
} catch (err) {
935+
console.error('Failed to import graph:', err);
936+
}
937+
};
938+
input.click();
939+
}, []);
940+
911941
const toggleCategory = useCallback((cat: string) => {
912942
setCollapsedCategories(prev => {
913943
const next = new Set(prev);
@@ -969,6 +999,20 @@ export function FlowsPanel() {
969999
<Trash2 size={14} />
9701000
</button>
9711001
)}
1002+
<button
1003+
onClick={handleExportGraph}
1004+
className="flex items-center gap-1 px-2 py-1.5 rounded-md text-sm text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 transition-colors"
1005+
title="Export graph as JSON"
1006+
>
1007+
<Download size={14} />
1008+
</button>
1009+
<button
1010+
onClick={handleImportGraph}
1011+
className="flex items-center gap-1 px-2 py-1.5 rounded-md text-sm text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 transition-colors"
1012+
title="Import graph from JSON"
1013+
>
1014+
<Upload size={14} />
1015+
</button>
9721016
{saveStatus && (
9731017
<span className={`text-xs ${saveStatus.startsWith('Error') ? 'text-red-400' : 'text-green-400'}`}>
9741018
{saveStatus}

0 commit comments

Comments
 (0)