@d34dman/flowdrop 0.0.33 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -620,4 +620,26 @@
620
620
  stroke: var(--flowdrop-edge-data-color-selected);
621
621
  stroke-width: 2;
622
622
  }
623
+
624
+ /* Loopback Edge: Dashed gray line for loop iteration connections */
625
+ :global(.flowdrop--edge--loopback path.svelte-flow__edge-path) {
626
+ stroke: var(--flowdrop-edge-loopback-color);
627
+ stroke-width: var(--flowdrop-edge-loopback-width);
628
+ stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
629
+ opacity: var(--flowdrop-edge-loopback-opacity);
630
+ }
631
+
632
+ :global(.flowdrop--edge--loopback:hover path.svelte-flow__edge-path) {
633
+ stroke: var(--flowdrop-edge-loopback-color-hover);
634
+ stroke-width: var(--flowdrop-edge-loopback-width-hover);
635
+ opacity: 1;
636
+ }
637
+
638
+ :global(.flowdrop--edge--loopback.selected path.svelte-flow__edge-path) {
639
+ stroke: var(--flowdrop-edge-loopback-color-selected);
640
+ stroke-width: var(--flowdrop-edge-loopback-width-hover);
641
+ stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
642
+ filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
643
+ opacity: 1;
644
+ }
623
645
  </style>
@@ -14,7 +14,7 @@
14
14
  * import { getStatusColor, createDefaultExecutionInfo } from "@d34dman/flowdrop/core";
15
15
  * ```
16
16
  */
17
- export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions } from '../types/index.js';
17
+ export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions, EdgeCategory } from '../types/index.js';
18
18
  export type { WorkflowEditorConfig, EditorFeatures, UIConfig, APIConfig, ExecutionConfig, StorageConfig } from '../types/config.js';
19
19
  export type { AuthProvider, StaticAuthConfig, CallbackAuthConfig } from '../types/auth.js';
20
20
  export type { WorkflowChangeType, FlowDropEventHandlers, FlowDropFeatures } from '../types/events.js';
@@ -35,6 +35,7 @@ export * from '../utils/colors.js';
35
35
  export * from '../utils/icons.js';
36
36
  export * from '../utils/config.js';
37
37
  export * from '../utils/nodeTypes.js';
38
+ export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
38
39
  export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
39
40
  export { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
40
41
  export { defaultEndpointConfig, createEndpointConfig } from '../config/endpoints.js';
@@ -38,6 +38,8 @@ export * from '../utils/icons.js';
38
38
  export * from '../utils/config.js';
39
39
  // Node type utilities
40
40
  export * from '../utils/nodeTypes.js';
41
+ // Connection utilities (including loopback edge detection)
42
+ export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
41
43
  // Form type utilities
42
44
  export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
43
45
  // ============================================================================
@@ -14,9 +14,10 @@ export declare function generateNodeId(nodeTypeId: string, existingNodes: Workfl
14
14
  * Edge category type for styling purposes
15
15
  * - trigger: For control flow connections (dataType: "trigger")
16
16
  * - tool: Dashed amber line for tool connections (dataType: "tool")
17
+ * - loopback: Dashed gray line for loop iteration connections (targets loop_back port)
17
18
  * - data: Normal gray line for all other data connections
18
19
  */
19
- export type EdgeCategory = 'trigger' | 'tool' | 'data';
20
+ export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
20
21
  /**
21
22
  * Edge styling configuration based on source port data type
22
23
  */
@@ -49,12 +50,25 @@ export declare class EdgeStylingHelper {
49
50
  static getPortDataType(node: WorkflowNodeType, portId: string, portType: 'input' | 'output'): string | null;
50
51
  /**
51
52
  * Determine the edge category based on source port data type
53
+ * Note: This method does not check for loopback edges.
54
+ * Use getEdgeCategoryWithLoopback() for full edge categorization.
55
+ *
52
56
  * @param sourcePortDataType - The data type of the source output port
53
57
  * @returns The edge category for styling
54
58
  */
55
59
  static getEdgeCategory(sourcePortDataType: string | null): EdgeCategory;
56
60
  /**
57
- * Apply custom styling to connection edges based on source port data type:
61
+ * Determine the full edge category including loopback detection
62
+ * Loopback edges take precedence over source port data type
63
+ *
64
+ * @param edge - The edge to categorize
65
+ * @param sourcePortDataType - The data type of the source output port
66
+ * @returns The edge category for styling
67
+ */
68
+ static getEdgeCategoryWithLoopback(edge: WorkflowEdge, sourcePortDataType: string | null): EdgeCategory;
69
+ /**
70
+ * Apply custom styling to connection edges based on edge type:
71
+ * - Loopback: Dashed gray line for loop iteration (targets loop_back port)
58
72
  * - Trigger ports: Solid black line with arrow
59
73
  * - Tool ports: Dashed amber line with arrow
60
74
  * - Data ports: Normal gray line with arrow
@@ -118,9 +132,24 @@ export declare class WorkflowOperationsHelper {
118
132
  */
119
133
  static exportWorkflow(workflow: Workflow | null): void;
120
134
  /**
121
- * Check if workflow has cycles
135
+ * Check if workflow has invalid cycles (excludes valid loopback cycles)
136
+ * Valid loopback cycles are used for ForEach node iteration and should not
137
+ * trigger a warning.
138
+ *
139
+ * @param nodes - Array of workflow nodes
140
+ * @param edges - Array of workflow edges
141
+ * @returns True if there are invalid (non-loopback) cycles
122
142
  */
123
143
  static checkWorkflowCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
144
+ /**
145
+ * Check if workflow has any cycles (including valid loopback cycles)
146
+ * Use this when you need to detect ALL cycles regardless of type.
147
+ *
148
+ * @param nodes - Array of workflow nodes
149
+ * @param edges - Array of workflow edges
150
+ * @returns True if any cycle exists
151
+ */
152
+ static checkWorkflowHasAnyCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
124
153
  }
125
154
  /**
126
155
  * Configuration helper
@@ -3,7 +3,7 @@
3
3
  * Contains business logic for workflow operations
4
4
  */
5
5
  import { MarkerType } from '@xyflow/svelte';
6
- import { hasCycles } from '../utils/connections.js';
6
+ import { hasCycles, hasInvalidCycles, isLoopbackEdge } from '../utils/connections.js';
7
7
  import { workflowApi, nodeApi, setEndpointConfig } from '../services/api.js';
8
8
  import { v4 as uuidv4 } from 'uuid';
9
9
  import { workflowActions } from '../stores/workflowStore.js';
@@ -104,6 +104,9 @@ export class EdgeStylingHelper {
104
104
  }
105
105
  /**
106
106
  * Determine the edge category based on source port data type
107
+ * Note: This method does not check for loopback edges.
108
+ * Use getEdgeCategoryWithLoopback() for full edge categorization.
109
+ *
107
110
  * @param sourcePortDataType - The data type of the source output port
108
111
  * @returns The edge category for styling
109
112
  */
@@ -118,7 +121,25 @@ export class EdgeStylingHelper {
118
121
  return 'data';
119
122
  }
120
123
  /**
121
- * Apply custom styling to connection edges based on source port data type:
124
+ * Determine the full edge category including loopback detection
125
+ * Loopback edges take precedence over source port data type
126
+ *
127
+ * @param edge - The edge to categorize
128
+ * @param sourcePortDataType - The data type of the source output port
129
+ * @returns The edge category for styling
130
+ */
131
+ static getEdgeCategoryWithLoopback(edge, sourcePortDataType) {
132
+ // Loopback edges are identified by their target handle
133
+ // Check this first as it takes precedence
134
+ if (isLoopbackEdge(edge)) {
135
+ return 'loopback';
136
+ }
137
+ // Fall back to source port data type categorization
138
+ return this.getEdgeCategory(sourcePortDataType);
139
+ }
140
+ /**
141
+ * Apply custom styling to connection edges based on edge type:
142
+ * - Loopback: Dashed gray line for loop iteration (targets loop_back port)
122
143
  * - Trigger ports: Solid black line with arrow
123
144
  * - Tool ports: Dashed amber line with arrow
124
145
  * - Data ports: Normal gray line with arrow
@@ -130,17 +151,30 @@ export class EdgeStylingHelper {
130
151
  const sourcePortDataType = sourcePortId
131
152
  ? this.getPortDataType(sourceNode, sourcePortId, 'output')
132
153
  : null;
133
- // Determine edge category based on source port data type
134
- const edgeCategory = this.getEdgeCategory(sourcePortDataType);
154
+ // Determine edge category (loopback takes precedence)
155
+ const edgeCategory = this.getEdgeCategoryWithLoopback(edge, sourcePortDataType);
135
156
  // Edge color constants (matching CSS tokens in base.css)
136
157
  const EDGE_COLORS = {
137
158
  trigger: '#111827', // --color-ref-gray-900
138
159
  tool: '#f59e0b', // --color-ref-amber-500
160
+ loopback: '#6b7280', // --color-ref-gray-500
139
161
  data: '#9ca3af' // --color-ref-gray-400
140
162
  };
141
163
  // Apply styling based on edge category
142
164
  // CSS classes handle styling via tokens; inline styles are fallback
143
165
  switch (edgeCategory) {
166
+ case 'loopback':
167
+ // Loopback edges: dashed gray line for loop iteration
168
+ edge.style =
169
+ 'stroke: var(--flowdrop-edge-loopback-color); stroke-dasharray: 5 5; stroke-width: var(--flowdrop-edge-loopback-width);';
170
+ edge.class = 'flowdrop--edge--loopback';
171
+ edge.markerEnd = {
172
+ type: MarkerType.ArrowClosed,
173
+ width: 14,
174
+ height: 14,
175
+ color: EDGE_COLORS.loopback
176
+ };
177
+ break;
144
178
  case 'trigger':
145
179
  // Trigger edges: solid dark line for control flow
146
180
  edge.style =
@@ -183,7 +217,7 @@ export class EdgeStylingHelper {
183
217
  metadata: {
184
218
  ...(edge.data?.metadata || {}),
185
219
  edgeType: edgeCategory,
186
- sourcePortDataType: sourcePortDataType || undefined
220
+ sourcePortDataType: sourcePortDataType ?? undefined
187
221
  },
188
222
  targetNodeType: targetNode.type,
189
223
  targetCategory: targetNode.data.metadata.category
@@ -484,9 +518,26 @@ export class WorkflowOperationsHelper {
484
518
  URL.revokeObjectURL(url);
485
519
  }
486
520
  /**
487
- * Check if workflow has cycles
521
+ * Check if workflow has invalid cycles (excludes valid loopback cycles)
522
+ * Valid loopback cycles are used for ForEach node iteration and should not
523
+ * trigger a warning.
524
+ *
525
+ * @param nodes - Array of workflow nodes
526
+ * @param edges - Array of workflow edges
527
+ * @returns True if there are invalid (non-loopback) cycles
488
528
  */
489
529
  static checkWorkflowCycles(nodes, edges) {
530
+ return hasInvalidCycles(nodes, edges);
531
+ }
532
+ /**
533
+ * Check if workflow has any cycles (including valid loopback cycles)
534
+ * Use this when you need to detect ALL cycles regardless of type.
535
+ *
536
+ * @param nodes - Array of workflow nodes
537
+ * @param edges - Array of workflow edges
538
+ * @returns True if any cycle exists
539
+ */
540
+ static checkWorkflowHasAnyCycles(nodes, edges) {
490
541
  return hasCycles(nodes, edges);
491
542
  }
492
543
  }
@@ -1157,6 +1157,15 @@
1157
1157
  --flowdrop-edge-data-color-hover: var(--color-ref-gray-500);
1158
1158
  --flowdrop-edge-data-color-selected: var(--color-ref-violet-600);
1159
1159
 
1160
+ /* Loopback edge styling tokens */
1161
+ --flowdrop-edge-loopback-color: var(--color-ref-gray-500);
1162
+ --flowdrop-edge-loopback-color-hover: var(--color-ref-gray-600);
1163
+ --flowdrop-edge-loopback-color-selected: var(--color-ref-violet-600);
1164
+ --flowdrop-edge-loopback-width: 1.5px;
1165
+ --flowdrop-edge-loopback-width-hover: 2.5px;
1166
+ --flowdrop-edge-loopback-dasharray: 5 5;
1167
+ --flowdrop-edge-loopback-opacity: 0.85;
1168
+
1160
1169
  /* Tool node theming tokens */
1161
1170
  --flowdrop-tool-node-color: var(--color-ref-amber-500);
1162
1171
  --flowdrop-tool-node-color-light: var(--color-ref-amber-50);
@@ -656,13 +656,14 @@ export interface WorkflowNode extends Node {
656
656
  };
657
657
  }
658
658
  /**
659
- * Edge category types based on source port data type
659
+ * Edge category types based on source port data type or target handle
660
660
  * Used for visual styling of edges on the canvas
661
661
  * - trigger: For control flow connections (dataType: "trigger")
662
662
  * - tool: Dashed amber line for tool connections (dataType: "tool")
663
+ * - loopback: Dashed gray line for loop iteration (targets loop_back port)
663
664
  * - data: Normal gray line for all other data connections
664
665
  */
665
- export type EdgeCategory = 'trigger' | 'tool' | 'data';
666
+ export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
666
667
  /**
667
668
  * Extended edge type for workflows
668
669
  */
@@ -2,6 +2,29 @@
2
2
  * Connection validation utilities for FlowDrop
3
3
  */
4
4
  import type { NodeMetadata, NodePort, NodeDataType, WorkflowNode, WorkflowEdge, PortConfig, PortDataTypeConfig } from '../types/index.js';
5
+ /**
6
+ * Determines if an edge is a loopback edge.
7
+ * Loopback edges target the special `loop_back` input port on ForEach nodes.
8
+ * These edges are used to trigger the next iteration in a loop construct.
9
+ *
10
+ * @param edge - The edge to check
11
+ * @returns True if the edge is a loopback edge
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const edge = { targetHandle: "foreach.1-input-loop_back", ... };
16
+ * const isLoop = isLoopbackEdge(edge); // true
17
+ * ```
18
+ */
19
+ export declare function isLoopbackEdge(edge: WorkflowEdge): boolean;
20
+ /**
21
+ * Checks if a cycle consists entirely of loopback edges.
22
+ * A valid loopback cycle only contains edges that target loop_back ports.
23
+ *
24
+ * @param cycleEdges - Array of edges that form a cycle
25
+ * @returns True if all edges in the cycle are loopback edges
26
+ */
27
+ export declare function isValidLoopbackCycle(cycleEdges: WorkflowEdge[]): boolean;
5
28
  /**
6
29
  * Configurable port compatibility checker
7
30
  */
@@ -71,8 +94,31 @@ export declare function getConnectionSuggestions(nodeId: string, nodes: Workflow
71
94
  }>;
72
95
  /**
73
96
  * Check if a workflow has any cycles (prevent infinite loops)
97
+ * Note: This function detects ALL cycles, including valid loopback cycles.
98
+ * Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
99
+ *
100
+ * @param nodes - Array of workflow nodes
101
+ * @param edges - Array of workflow edges
102
+ * @returns True if any cycle exists in the workflow
74
103
  */
75
104
  export declare function hasCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
105
+ /**
106
+ * Check if a workflow has any invalid cycles (non-loopback cycles).
107
+ * This excludes valid loopback cycles used for ForEach iteration.
108
+ * Only cycles that could cause infinite execution are detected.
109
+ *
110
+ * @param nodes - Array of workflow nodes
111
+ * @param edges - Array of workflow edges
112
+ * @returns True if any invalid (non-loopback) cycle exists
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * // A cycle through a loopback edge is valid (returns false)
117
+ * // A cycle through regular data edges is invalid (returns true)
118
+ * const hasInvalid = hasInvalidCycles(nodes, edges);
119
+ * ```
120
+ */
121
+ export declare function hasInvalidCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
76
122
  /**
77
123
  * Get the execution order for a workflow (topological sort)
78
124
  */
@@ -1,6 +1,39 @@
1
1
  /**
2
2
  * Connection validation utilities for FlowDrop
3
3
  */
4
+ /**
5
+ * Loopback port name constant
6
+ * This is the standard input port name used for loop iteration triggers
7
+ */
8
+ const LOOPBACK_PORT_NAME = "loop_back";
9
+ /**
10
+ * Determines if an edge is a loopback edge.
11
+ * Loopback edges target the special `loop_back` input port on ForEach nodes.
12
+ * These edges are used to trigger the next iteration in a loop construct.
13
+ *
14
+ * @param edge - The edge to check
15
+ * @returns True if the edge is a loopback edge
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const edge = { targetHandle: "foreach.1-input-loop_back", ... };
20
+ * const isLoop = isLoopbackEdge(edge); // true
21
+ * ```
22
+ */
23
+ export function isLoopbackEdge(edge) {
24
+ const targetHandle = edge.targetHandle ?? "";
25
+ return targetHandle.includes(`-input-${LOOPBACK_PORT_NAME}`);
26
+ }
27
+ /**
28
+ * Checks if a cycle consists entirely of loopback edges.
29
+ * A valid loopback cycle only contains edges that target loop_back ports.
30
+ *
31
+ * @param cycleEdges - Array of edges that form a cycle
32
+ * @returns True if all edges in the cycle are loopback edges
33
+ */
34
+ export function isValidLoopbackCycle(cycleEdges) {
35
+ return cycleEdges.every((edge) => isLoopbackEdge(edge));
36
+ }
4
37
  /**
5
38
  * Configurable port compatibility checker
6
39
  */
@@ -225,6 +258,12 @@ export function getConnectionSuggestions(nodeId, nodes, nodeTypes) {
225
258
  }
226
259
  /**
227
260
  * Check if a workflow has any cycles (prevent infinite loops)
261
+ * Note: This function detects ALL cycles, including valid loopback cycles.
262
+ * Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
263
+ *
264
+ * @param nodes - Array of workflow nodes
265
+ * @param edges - Array of workflow edges
266
+ * @returns True if any cycle exists in the workflow
228
267
  */
229
268
  export function hasCycles(nodes, edges) {
230
269
  const visited = new Set();
@@ -254,6 +293,58 @@ export function hasCycles(nodes, edges) {
254
293
  }
255
294
  return false;
256
295
  }
296
+ /**
297
+ * Check if a workflow has any invalid cycles (non-loopback cycles).
298
+ * This excludes valid loopback cycles used for ForEach iteration.
299
+ * Only cycles that could cause infinite execution are detected.
300
+ *
301
+ * @param nodes - Array of workflow nodes
302
+ * @param edges - Array of workflow edges
303
+ * @returns True if any invalid (non-loopback) cycle exists
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * // A cycle through a loopback edge is valid (returns false)
308
+ * // A cycle through regular data edges is invalid (returns true)
309
+ * const hasInvalid = hasInvalidCycles(nodes, edges);
310
+ * ```
311
+ */
312
+ export function hasInvalidCycles(nodes, edges) {
313
+ // Filter out loopback edges - these create valid cycles for loop iteration
314
+ const nonLoopbackEdges = edges.filter((edge) => !isLoopbackEdge(edge));
315
+ // Check for cycles using only non-loopback edges
316
+ const visited = new Set();
317
+ const recursionStack = new Set();
318
+ /**
319
+ * DFS utility to detect cycles in the graph
320
+ * @param nodeId - Current node being visited
321
+ * @returns True if a cycle is found from this node
322
+ */
323
+ function hasCycleUtil(nodeId) {
324
+ if (recursionStack.has(nodeId))
325
+ return true;
326
+ if (visited.has(nodeId))
327
+ return false;
328
+ visited.add(nodeId);
329
+ recursionStack.add(nodeId);
330
+ // Get all outgoing non-loopback edges from this node
331
+ const outgoingEdges = nonLoopbackEdges.filter((e) => e.source === nodeId);
332
+ for (const edge of outgoingEdges) {
333
+ if (hasCycleUtil(edge.target))
334
+ return true;
335
+ }
336
+ recursionStack.delete(nodeId);
337
+ return false;
338
+ }
339
+ // Check each node for cycles
340
+ for (const node of nodes) {
341
+ if (!visited.has(node.id)) {
342
+ if (hasCycleUtil(node.id))
343
+ return true;
344
+ }
345
+ }
346
+ return false;
347
+ }
257
348
  /**
258
349
  * Get the execution order for a workflow (topological sort)
259
350
  */
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@d34dman/flowdrop",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "0.0.33",
5
+ "version": "0.0.34",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "build": "vite build && npm run prepack",