Skip to content

Commit 7b66313

Browse files
committed
refactor(task-graph): apply PR #448 review feedback
Four small fixes flagged by Copilot review on the Workflow decomposition: 1. WorkflowEventBridge.detach() now also tears down the streaming subscription and clears _streamingUnsub. Without this, a graph swap or reset() during a run would leak the old graph's streaming handlers wired to this workflow's events emitter. 2. runLoopAutoConnect / consumePendingConnect now return the error message instead of just logging. Callers (Workflow#run for the loop- builder branch, Workflow#autoConnectLoopTask, LoopBuilderContext# finalizeAndReturn) propagate the error onto the parent workflow's builder via the new WorkflowBuilder#setError(message). This restores parity with the pre-refactor behavior where loop auto-connect failures surfaced on Workflow.error, not just in the logger. 3. Workflow.ts switches `import { TaskOutputRepository }` to `import type` since it's only used in type positions. 4. WorkflowPipe.connect() now uses the canonical DATAFLOW_ALL_PORTS constant instead of the hard-coded "*" sentinel.
1 parent bfde777 commit 7b66313

5 files changed

Lines changed: 54 additions & 15 deletions

File tree

packages/task-graph/src/task-graph/LoopBuilderContext.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,21 @@ export interface PendingLoopConnect {
1919
/**
2020
* Runs deferred auto-connect for a loop iterator task on the parent
2121
* workflow's graph. Extracted as a free function so it can be invoked
22-
* from both {@link LoopBuilderContext.autoConnectLoopTask} and from
22+
* from both {@link LoopBuilderContext.consumePendingConnect} and from
2323
* the parent {@link Workflow}'s public delegate method (the parent is
2424
* not itself in loop-builder mode and has no context of its own).
25+
*
26+
* Returns the error message if auto-connect failed, otherwise undefined.
27+
* On failure the iterator task is removed from the parent graph; the
28+
* caller is responsible for surfacing the error onto the parent
29+
* workflow's `.error` (matches the non-loop auto-connect path).
2530
*/
26-
export function runLoopAutoConnect(parentGraph: TaskGraph, pending: PendingLoopConnect): void {
31+
export function runLoopAutoConnect(
32+
parentGraph: TaskGraph,
33+
pending: PendingLoopConnect
34+
): string | undefined {
2735
const { parent, iteratorTask } = pending;
28-
if (parentGraph.getTargetDataflows(parent.id).length !== 0) return;
36+
if (parentGraph.getTargetDataflows(parent.id).length !== 0) return undefined;
2937

3038
const nodes = parentGraph.getTasks();
3139
const parentIndex = nodes.findIndex((n) => n.id === parent.id);
@@ -36,9 +44,12 @@ export function runLoopAutoConnect(parentGraph: TaskGraph, pending: PendingLoopC
3644

3745
const result = autoConnect(parentGraph, parent, iteratorTask, { earlierTasks });
3846
if (result.error) {
39-
getLogger().error(result.error + " Task not added.");
47+
const message = result.error + " Task not added.";
48+
getLogger().error(message);
4049
parentGraph.removeTask(iteratorTask.id);
50+
return message;
4151
}
52+
return undefined;
4253
}
4354

4455
/**
@@ -66,18 +77,28 @@ export class LoopBuilderContext {
6677
this.iteratorTask.validateAcyclic();
6778
}
6879

69-
/** Runs auto-connect for the pending loop connect (if any), then clears it. */
70-
public consumePendingConnect(): void {
80+
/**
81+
* Runs auto-connect for the pending loop connect (if any), then clears it.
82+
* Returns the error message if auto-connect failed, otherwise undefined,
83+
* so the caller can propagate the failure to the parent workflow's `.error`.
84+
*/
85+
public consumePendingConnect(): string | undefined {
7186
const pending = this.pendingLoopConnect;
72-
if (!pending) return;
73-
runLoopAutoConnect(this.parent.graph, pending);
87+
if (!pending) return undefined;
88+
const error = runLoopAutoConnect(this.parent.graph, pending);
7489
this.pendingLoopConnect = undefined;
90+
return error;
7591
}
7692

77-
/** Finalizes the template and returns the parent workflow. */
93+
/**
94+
* Finalizes the template and returns the parent workflow. Any deferred
95+
* auto-connect error is surfaced onto the parent's `.error` to match the
96+
* non-loop auto-connect path.
97+
*/
7898
public finalizeAndReturn(childGraph: TaskGraph): Workflow {
7999
this.finalizeTemplate(childGraph);
80-
this.consumePendingConnect();
100+
const error = this.consumePendingConnect();
101+
if (error) this.parent.builder.setError(error);
81102
return this.parent;
82103
}
83104
}

packages/task-graph/src/task-graph/Workflow.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import type { EventParameters } from "@workglow/util";
88
import { EventEmitter, ServiceRegistry } from "@workglow/util";
9-
import { TaskOutputRepository } from "../storage/TaskOutputRepository";
9+
import type { TaskOutputRepository } from "../storage/TaskOutputRepository";
1010
import type { ConditionFn } from "../task/ConditionalTask";
1111
import { GraphAsTask } from "../task/GraphAsTask";
1212
import type { ITask, ITaskConstructor } from "../task/ITask";
@@ -243,7 +243,8 @@ export class Workflow<
243243
const loopContext = this._builder.loopContext;
244244
if (loopContext) {
245245
loopContext.finalizeTemplate(this._graph);
246-
loopContext.consumePendingConnect();
246+
const error = loopContext.consumePendingConnect();
247+
if (error) loopContext.parent.builder.setError(error);
247248
return loopContext.parent.run(input as any, config) as Promise<
248249
PropertyArrayGraphResult<Output>
249250
>;
@@ -588,7 +589,8 @@ export class Workflow<
588589
*/
589590
public autoConnectLoopTask(pending?: { parent: ITask; iteratorTask: ITask }): void {
590591
if (!pending) return;
591-
runLoopAutoConnect(this._graph, pending);
592+
const error = runLoopAutoConnect(this._graph, pending);
593+
if (error) this._builder.setError(error);
592594
}
593595

594596
/**

packages/task-graph/src/task-graph/WorkflowBuilder.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ export class WorkflowBuilder {
5959
return this._loopContext;
6060
}
6161

62+
/**
63+
* Surfaces an error onto this builder's error slot. Used by deferred
64+
* loop-builder auto-connect (which runs from a child Workflow but reports
65+
* the failure onto the parent's `.error`, matching the non-loop path).
66+
*/
67+
public setError(message: string): void {
68+
this._error = message;
69+
}
70+
6271
/** Clears pending state. Called by the facade's graph setter and reset(). */
6372
public resetState(): void {
6473
this._dataFlows = [];

packages/task-graph/src/task-graph/WorkflowEventBridge.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export class WorkflowEventBridge {
5959
graph.off("dataflow_removed", this._onChanged);
6060
this._entitlementUnsub?.();
6161
this._entitlementUnsub = undefined;
62+
// Tear down any streaming subscription that's still tied to this graph.
63+
// Without this, a graph swap or reset() during a run would leave the old
64+
// graph's streaming handlers wired to this workflow's events emitter.
65+
this._streamingUnsub?.();
66+
this._streamingUnsub = undefined;
6267
this._attachedGraph = undefined;
6368
}
6469

packages/task-graph/src/task-graph/WorkflowPipe.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { ITask } from "../task/ITask";
99
import type { DataPorts } from "../task/TaskTypes";
1010
import type { PipeFunction, Taskish } from "./Conversions";
1111
import { ensureTask } from "./Conversions";
12-
import { Dataflow } from "./Dataflow";
12+
import { Dataflow, DATAFLOW_ALL_PORTS } from "./Dataflow";
1313
import type { ITaskGraph } from "./ITaskGraph";
1414
import type { IWorkflow } from "./IWorkflow";
1515
import type { CompoundMergeStrategy } from "./TaskGraphRunner";
@@ -31,7 +31,9 @@ export function connect(
3131
target: ITask<any, any, any>,
3232
workflow: IWorkflow<any, any>
3333
): void {
34-
workflow.graph.addDataflow(new Dataflow(source.id, "*", target.id, "*"));
34+
workflow.graph.addDataflow(
35+
new Dataflow(source.id, DATAFLOW_ALL_PORTS, target.id, DATAFLOW_ALL_PORTS)
36+
);
3537
}
3638

3739
export function pipe<A extends DataPorts, B extends DataPorts>(

0 commit comments

Comments
 (0)