@flowdrop/flowdrop 1.15.0 → 2.0.0-beta.2

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.
Files changed (235) hide show
  1. package/CHANGELOG.md +508 -0
  2. package/MIGRATION-2.0.md +629 -0
  3. package/README.md +23 -23
  4. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  5. package/dist/adapters/WorkflowAdapter.js +14 -8
  6. package/dist/adapters/agentspec/AgentSpecAdapter.js +7 -7
  7. package/dist/api/enhanced-client.js +6 -11
  8. package/dist/chat/batchFeedback.d.ts +39 -0
  9. package/dist/chat/batchFeedback.js +51 -0
  10. package/dist/commands/executor.js +15 -1
  11. package/dist/commands/storeIntegration.svelte.d.ts +4 -1
  12. package/dist/commands/storeIntegration.svelte.js +26 -21
  13. package/dist/commands/types.d.ts +2 -0
  14. package/dist/components/App.svelte +163 -192
  15. package/dist/components/App.svelte.d.ts +47 -8
  16. package/dist/components/ConfigForm.svelte +77 -49
  17. package/dist/components/ConfigModal.svelte +7 -2
  18. package/dist/components/ConnectionLine.svelte +4 -2
  19. package/dist/components/Navbar.svelte +61 -1
  20. package/dist/components/NodeSidebar.svelte +27 -45
  21. package/dist/components/NodeStatusOverlay.svelte +94 -6
  22. package/dist/components/NodeSwapPicker.svelte +10 -8
  23. package/dist/components/PipelineStatus.svelte +22 -68
  24. package/dist/components/PipelineStatus.svelte.d.ts +3 -0
  25. package/dist/components/PortCoordinateTracker.svelte +5 -6
  26. package/dist/components/SchemaForm.stories.svelte +1 -3
  27. package/dist/components/SchemaForm.svelte +22 -27
  28. package/dist/components/SchemaForm.svelte.d.ts +0 -8
  29. package/dist/components/SettingsModal.svelte +8 -3
  30. package/dist/components/SettingsPanel.svelte +20 -4
  31. package/dist/components/SwapMappingEditor.svelte +67 -49
  32. package/dist/components/SwapMappingEditor.svelte.d.ts +0 -2
  33. package/dist/components/UniversalNode.svelte +9 -7
  34. package/dist/components/WorkflowEditor.svelte +121 -111
  35. package/dist/components/WorkflowEditor.svelte.d.ts +21 -10
  36. package/dist/components/chat/AIChatPanel.svelte +98 -89
  37. package/dist/components/chat/AIChatPanel.svelte.d.ts +0 -4
  38. package/dist/components/chat/CommandPreview.svelte +2 -1
  39. package/dist/components/console/CommandConsole.svelte +7 -5
  40. package/dist/components/console/ConsoleAutocomplete.svelte +10 -11
  41. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +6 -0
  42. package/dist/components/console/ConsoleInput.svelte +15 -6
  43. package/dist/components/console/ConsoleOutput.svelte +2 -1
  44. package/dist/components/form/FormArray.svelte +5 -9
  45. package/dist/components/form/FormArray.svelte.d.ts +2 -1
  46. package/dist/components/form/FormAutocomplete.svelte +16 -15
  47. package/dist/components/form/FormField.svelte +4 -2
  48. package/dist/components/form/FormFieldLight.svelte +34 -3
  49. package/dist/components/form/FormFieldLight.svelte.d.ts +12 -0
  50. package/dist/components/form/FormMarkdownEditor.svelte +9 -4
  51. package/dist/components/form/FormRangeField.svelte +1 -0
  52. package/dist/components/form/FormTemplateEditor.svelte +11 -3
  53. package/dist/components/form/FormToggle.svelte +5 -12
  54. package/dist/components/form/FormToggle.svelte.d.ts +4 -2
  55. package/dist/components/form/FormUISchemaRenderer.svelte +3 -1
  56. package/dist/components/form/templateAutocomplete.js +1 -5
  57. package/dist/components/form/types.d.ts +1 -14
  58. package/dist/components/interrupt/FormPrompt.svelte +3 -2
  59. package/dist/components/interrupt/InterruptBubble.svelte +25 -17
  60. package/dist/components/interrupt/ReviewPrompt.svelte +10 -3
  61. package/dist/components/interrupt/TextInputPrompt.svelte +2 -1
  62. package/dist/components/layouts/MainLayout.svelte +20 -13
  63. package/dist/components/layouts/MainLayout.svelte.d.ts +4 -0
  64. package/dist/components/nodes/AtomNode.svelte +17 -5
  65. package/dist/components/nodes/GatewayNode.svelte +19 -10
  66. package/dist/components/nodes/IdeaNode.svelte +7 -0
  67. package/dist/components/nodes/SimpleNode.svelte +11 -6
  68. package/dist/components/nodes/SquareNode.svelte +15 -8
  69. package/dist/components/nodes/TerminalNode.svelte +9 -4
  70. package/dist/components/nodes/ToolNode.svelte +7 -1
  71. package/dist/components/nodes/WorkflowNode.svelte +16 -7
  72. package/dist/components/playground/ChatInput.svelte +11 -14
  73. package/dist/components/playground/ChatPanel.svelte +6 -49
  74. package/dist/components/playground/ChatPanel.svelte.d.ts +0 -14
  75. package/dist/components/playground/ControlPanel.svelte +134 -123
  76. package/dist/components/playground/ControlPanel.svelte.d.ts +3 -0
  77. package/dist/components/playground/ExecutionLogs.svelte +11 -9
  78. package/dist/components/playground/InputCollector.svelte +11 -9
  79. package/dist/components/playground/MessageStream.svelte +17 -23
  80. package/dist/components/playground/PipelineKanbanView.svelte +69 -8
  81. package/dist/components/playground/PipelineKanbanView.svelte.d.ts +2 -0
  82. package/dist/components/playground/PipelinePanel.svelte +31 -8
  83. package/dist/components/playground/PipelinePanel.svelte.d.ts +2 -0
  84. package/dist/components/playground/PipelineTableView.svelte +188 -44
  85. package/dist/components/playground/PipelineTableView.svelte.d.ts +2 -0
  86. package/dist/components/playground/Playground.svelte +154 -105
  87. package/dist/components/playground/Playground.svelte.d.ts +5 -0
  88. package/dist/components/playground/PlaygroundApp.svelte +11 -1
  89. package/dist/components/playground/PlaygroundApp.svelte.d.ts +6 -0
  90. package/dist/components/playground/PlaygroundModal.svelte +18 -3
  91. package/dist/components/playground/PlaygroundModal.svelte.d.ts +6 -0
  92. package/dist/components/playground/PlaygroundStudio.svelte +40 -32
  93. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +6 -0
  94. package/dist/components/playground/SessionManager.svelte +9 -12
  95. package/dist/components/playground/pipelineViewUtils.svelte.d.ts +30 -1
  96. package/dist/components/playground/pipelineViewUtils.svelte.js +40 -3
  97. package/dist/config/endpoints.d.ts +23 -7
  98. package/dist/config/endpoints.js +30 -10
  99. package/dist/core/index.d.ts +5 -6
  100. package/dist/core/index.js +8 -12
  101. package/dist/display/index.d.ts +6 -3
  102. package/dist/display/index.js +7 -5
  103. package/dist/editor/index.d.ts +20 -21
  104. package/dist/editor/index.js +26 -36
  105. package/dist/form/code.d.ts +25 -15
  106. package/dist/form/code.js +44 -41
  107. package/dist/form/fieldRegistry.d.ts +17 -13
  108. package/dist/form/fieldRegistry.js +32 -12
  109. package/dist/form/full.d.ts +19 -14
  110. package/dist/form/full.js +26 -28
  111. package/dist/form/index.d.ts +3 -4
  112. package/dist/form/index.js +6 -5
  113. package/dist/form/markdown.d.ts +13 -8
  114. package/dist/form/markdown.js +22 -23
  115. package/dist/helpers/proximityConnect.d.ts +3 -2
  116. package/dist/helpers/proximityConnect.js +2 -5
  117. package/dist/helpers/workflowEditorHelper.d.ts +14 -5
  118. package/dist/helpers/workflowEditorHelper.js +28 -25
  119. package/dist/index.d.ts +28 -24
  120. package/dist/index.js +27 -50
  121. package/dist/messages/defaults.d.ts +2 -5
  122. package/dist/messages/defaults.js +3 -6
  123. package/dist/messages/index.d.ts +0 -1
  124. package/dist/messages/index.js +0 -1
  125. package/dist/mocks/app-forms.d.ts +6 -2
  126. package/dist/mocks/app-forms.js +11 -4
  127. package/dist/openapi/v1/openapi.yaml +3 -3
  128. package/dist/playground/index.d.ts +4 -5
  129. package/dist/playground/index.js +4 -32
  130. package/dist/playground/mount.d.ts +25 -0
  131. package/dist/playground/mount.js +50 -20
  132. package/dist/registry/{BaseRegistry.d.ts → BaseRegistry.svelte.d.ts} +22 -1
  133. package/dist/registry/{BaseRegistry.js → BaseRegistry.svelte.js} +37 -1
  134. package/dist/registry/builtinFormats.d.ts +9 -18
  135. package/dist/registry/builtinFormats.js +9 -39
  136. package/dist/registry/builtinNodeTypes.d.ts +53 -0
  137. package/dist/registry/builtinNodeTypes.js +67 -0
  138. package/dist/registry/builtinNodes.d.ts +2 -64
  139. package/dist/registry/builtinNodes.js +7 -103
  140. package/dist/registry/index.d.ts +3 -4
  141. package/dist/registry/index.js +4 -6
  142. package/dist/registry/nodeComponentRegistry.d.ts +182 -15
  143. package/dist/registry/nodeComponentRegistry.js +235 -17
  144. package/dist/registry/workflowFormatRegistry.d.ts +14 -9
  145. package/dist/registry/workflowFormatRegistry.js +24 -8
  146. package/dist/{schema → schemas}/index.d.ts +2 -2
  147. package/dist/{schema → schemas}/index.js +2 -2
  148. package/dist/schemas/v1/workflow.schema.json +3 -3
  149. package/dist/services/agentSpecExecutionService.d.ts +0 -2
  150. package/dist/services/agentSpecExecutionService.js +0 -3
  151. package/dist/services/apiVariableService.d.ts +2 -1
  152. package/dist/services/apiVariableService.js +16 -47
  153. package/dist/services/autoSaveService.d.ts +7 -0
  154. package/dist/services/autoSaveService.js +6 -4
  155. package/dist/services/categoriesApi.js +3 -6
  156. package/dist/services/chatService.d.ts +9 -4
  157. package/dist/services/chatService.js +23 -28
  158. package/dist/services/draftStorage.d.ts +129 -13
  159. package/dist/services/draftStorage.js +185 -37
  160. package/dist/services/dynamicSchemaService.d.ts +2 -1
  161. package/dist/services/dynamicSchemaService.js +5 -22
  162. package/dist/services/globalSave.d.ts +13 -12
  163. package/dist/services/globalSave.js +29 -51
  164. package/dist/services/historyService.d.ts +9 -3
  165. package/dist/services/historyService.js +9 -3
  166. package/dist/services/interruptService.d.ts +15 -9
  167. package/dist/services/interruptService.js +35 -37
  168. package/dist/services/nodeExecutionService.d.ts +18 -3
  169. package/dist/services/nodeExecutionService.js +71 -45
  170. package/dist/services/playgroundService.d.ts +16 -10
  171. package/dist/services/playgroundService.js +42 -43
  172. package/dist/services/portConfigApi.js +3 -6
  173. package/dist/services/settingsService.d.ts +9 -4
  174. package/dist/services/settingsService.js +23 -12
  175. package/dist/services/variableService.d.ts +2 -1
  176. package/dist/services/variableService.js +2 -2
  177. package/dist/services/workflowStorage.js +6 -6
  178. package/dist/stores/apiContext.d.ts +56 -0
  179. package/dist/stores/apiContext.js +80 -0
  180. package/dist/stores/categoriesStore.svelte.d.ts +28 -23
  181. package/dist/stores/categoriesStore.svelte.js +69 -64
  182. package/dist/stores/getInstance.svelte.d.ts +39 -0
  183. package/dist/stores/getInstance.svelte.js +65 -0
  184. package/dist/stores/historyStore.svelte.d.ts +77 -93
  185. package/dist/stores/historyStore.svelte.js +134 -160
  186. package/dist/stores/instanceContainer.svelte.d.ts +111 -0
  187. package/dist/stores/instanceContainer.svelte.js +114 -0
  188. package/dist/stores/interruptStore.svelte.d.ts +112 -82
  189. package/dist/stores/interruptStore.svelte.js +253 -226
  190. package/dist/stores/pipelinePanelStore.svelte.d.ts +27 -3
  191. package/dist/stores/pipelinePanelStore.svelte.js +61 -14
  192. package/dist/stores/playgroundStore.svelte.d.ts +169 -222
  193. package/dist/stores/playgroundStore.svelte.js +513 -580
  194. package/dist/stores/portCoordinateStore.svelte.d.ts +57 -51
  195. package/dist/stores/portCoordinateStore.svelte.js +109 -98
  196. package/dist/stores/settingsStore.svelte.d.ts +4 -1
  197. package/dist/stores/settingsStore.svelte.js +47 -12
  198. package/dist/stores/workflowStore.svelte.d.ts +178 -213
  199. package/dist/stores/workflowStore.svelte.js +449 -501
  200. package/dist/stories/EdgeDecorator.svelte +5 -2
  201. package/dist/stories/NodeDecorator.svelte +5 -3
  202. package/dist/svelte-app.d.ts +60 -10
  203. package/dist/svelte-app.js +159 -54
  204. package/dist/types/auth.d.ts +9 -51
  205. package/dist/types/auth.js +4 -54
  206. package/dist/types/events.d.ts +6 -3
  207. package/dist/types/index.d.ts +37 -5
  208. package/dist/types/index.js +0 -1
  209. package/dist/types/navbar.d.ts +7 -0
  210. package/dist/types/playground.d.ts +18 -3
  211. package/dist/types/settings.d.ts +13 -0
  212. package/dist/types/settings.js +1 -0
  213. package/dist/utils/colors.d.ts +47 -21
  214. package/dist/utils/colors.js +69 -68
  215. package/dist/utils/connections.d.ts +9 -15
  216. package/dist/utils/connections.js +13 -32
  217. package/dist/utils/duration.d.ts +13 -0
  218. package/dist/utils/duration.js +45 -0
  219. package/dist/utils/edgeStyling.js +9 -5
  220. package/dist/utils/fetchWithAuth.d.ts +36 -15
  221. package/dist/utils/fetchWithAuth.js +53 -23
  222. package/dist/utils/icons.d.ts +5 -2
  223. package/dist/utils/icons.js +6 -5
  224. package/dist/utils/nodeSwap.d.ts +6 -2
  225. package/dist/utils/nodeSwap.js +62 -126
  226. package/dist/utils/nodeTypes.d.ts +17 -8
  227. package/dist/utils/nodeTypes.js +27 -20
  228. package/dist/utils/performanceUtils.js +7 -0
  229. package/package.json +7 -5
  230. package/dist/messages/deprecation.d.ts +0 -20
  231. package/dist/messages/deprecation.js +0 -33
  232. package/dist/registry/plugin.d.ts +0 -215
  233. package/dist/registry/plugin.js +0 -249
  234. package/dist/services/api.d.ts +0 -129
  235. package/dist/services/api.js +0 -217
@@ -1,23 +1,26 @@
1
1
  /**
2
2
  * Workflow Store for FlowDrop (Svelte 5 Runes)
3
3
  *
4
- * Provides global state management for workflows with dirty state tracking
5
- * and undo/redo history integration.
4
+ * Provides per-instance state management for workflows with dirty state
5
+ * tracking and undo/redo history integration.
6
6
  *
7
- * **Important: Single-instance only.** This store uses module-level singletons.
8
- * Only one FlowDrop editor instance per page is supported. Mounting multiple
9
- * FlowDrop editors on the same page will cause them to share workflow state.
7
+ * The reactive state lives in the {@link WorkflowStore} class — one per
8
+ * FlowDrop instance, created by `createFlowDropInstance()` and resolved in
9
+ * components via `getInstance().workflow`.
10
10
  *
11
11
  * @module stores/workflowStore
12
12
  */
13
13
  import { DEFAULT_WORKFLOW_FORMAT } from '../types/index.js';
14
- import { historyService } from '../services/historyService.js';
14
+ import { WORKFLOW_SCHEMA_VERSION } from '../schemas/index.js';
15
15
  /**
16
16
  * Safely build updated workflow metadata, providing defaults for required fields.
17
+ *
18
+ * Accepts loosely-typed legacy metadata so it can absorb 1.x documents (where
19
+ * the schema-version field was named `version`) during load-time healing.
17
20
  */
18
21
  function buildMetadata(existing, updates) {
19
22
  return {
20
- version: existing?.version ?? '1.0',
23
+ schemaVersion: existing?.schemaVersion ?? existing?.version ?? WORKFLOW_SCHEMA_VERSION,
21
24
  createdAt: existing?.createdAt ?? new Date().toISOString(),
22
25
  updatedAt: new Date().toISOString(),
23
26
  author: existing?.author,
@@ -28,336 +31,30 @@ function buildMetadata(existing, updates) {
28
31
  ...updates
29
32
  };
30
33
  }
31
- // =========================================================================
32
- // Core Workflow State (Svelte 5 Runes)
33
- // =========================================================================
34
- /** Global workflow state */
35
- let workflowState = $state(null);
36
- // =========================================================================
37
- // Dirty State Tracking (Version Counter)
38
- // =========================================================================
39
- /**
40
- * Monotonic edit version — bumps on every mutation action.
41
- *
42
- * Used for two purposes:
43
- * 1. O(1) dirty detection: isDirty = _editVersion !== _savedVersion
44
- * 2. Save verification protocol: include the version in the save request,
45
- * backend echoes it back. If the echoed version doesn't match, the save
46
- * didn't persist the version the client submitted.
47
- */
48
- let _editVersion = $state(0);
49
- /**
50
- * Edit version captured at the last successful save.
51
- * When _editVersion === _savedVersion, the workflow is clean.
52
- */
53
- let _savedVersion = $state(0);
54
- /**
55
- * Callback for dirty state changes
56
- *
57
- * Set by the App component to notify parent application.
58
- */
59
- let onDirtyStateChangeCallback = null;
60
- /**
61
- * Callback for workflow changes
62
- *
63
- * Set by the App component to notify parent application.
64
- */
65
- let onWorkflowChangeCallback = null;
66
- /**
67
- * Flag to track if we're currently restoring from history (undo/redo)
68
- *
69
- * When true, prevents pushing to history to avoid recursive loops.
70
- */
71
- let isRestoringFromHistory = false;
72
- /**
73
- * Flag to track if history recording is enabled
74
- *
75
- * Can be disabled for bulk operations or when history is not needed.
76
- */
77
- let historyEnabled = true;
78
- // =========================================================================
79
- // Getter Functions (Reactive State Access)
80
- // =========================================================================
81
- /**
82
- * Get the current workflow store value reactively
83
- *
84
- * @returns The current workflow or null
85
- */
86
- export function getWorkflowStore() {
87
- return workflowState;
88
- }
89
- /**
90
- * Get the current dirty state reactively
91
- *
92
- * Reads both _editVersion and _savedVersion, so Svelte tracks them
93
- * as reactive dependencies — any component using this will re-render
94
- * when dirty state changes.
95
- *
96
- * @returns true if there are unsaved changes
97
- */
98
- export function getIsDirty() {
99
- return _editVersion !== _savedVersion;
100
- }
101
- /**
102
- * Get the workflow ID reactively
103
- *
104
- * @returns The workflow ID or null
105
- */
106
- export function getWorkflowId() {
107
- return workflowState?.id ?? null;
108
- }
109
- /**
110
- * Get the workflow name reactively
111
- *
112
- * @returns The workflow name or 'Untitled Workflow'
113
- */
114
- export function getWorkflowName() {
115
- return workflowState?.name ?? 'Untitled Workflow';
116
- }
117
- /**
118
- * Get the workflow nodes reactively
119
- *
120
- * @returns Array of workflow nodes
121
- */
122
- export function getWorkflowNodes() {
123
- return workflowState?.nodes ?? [];
124
- }
125
- /**
126
- * Get the workflow edges reactively
127
- *
128
- * @returns Array of workflow edges
129
- */
130
- export function getWorkflowEdges() {
131
- return workflowState?.edges ?? [];
132
- }
133
- /**
134
- * Get the workflow metadata reactively
135
- *
136
- * @returns The workflow metadata with defaults
137
- */
138
- export function getWorkflowMetadata() {
139
- return (workflowState?.metadata ?? {
140
- version: '1.0.0',
141
- createdAt: new Date().toISOString(),
142
- updatedAt: new Date().toISOString(),
143
- versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
144
- updateNumber: 0
145
- });
146
- }
147
- /**
148
- * Get the current workflow format reactively
149
- *
150
- * @returns The workflow format string
151
- */
152
- export function getWorkflowFormat() {
153
- return workflowState?.metadata?.format ?? DEFAULT_WORKFLOW_FORMAT;
154
- }
155
- /**
156
- * Get workflow change summary reactively (useful for triggering saves)
157
- *
158
- * @returns Object with nodes, edges, and name
159
- */
160
- export function getWorkflowChanged() {
161
- return {
162
- nodes: getWorkflowNodes(),
163
- edges: getWorkflowEdges(),
164
- name: getWorkflowName()
165
- };
166
- }
167
- /**
168
- * Get workflow validation state reactively
169
- *
170
- * @returns Validation info object
171
- */
172
- export function getWorkflowValidation() {
173
- const nodes = getWorkflowNodes();
174
- const edges = getWorkflowEdges();
175
- return {
176
- hasNodes: nodes.length > 0,
177
- hasEdges: edges.length > 0,
178
- nodeCount: nodes.length,
179
- edgeCount: edges.length,
180
- isValid: nodes.length > 0 && edges.length >= 0
181
- };
182
- }
183
- /**
184
- * Get workflow metadata change summary reactively
185
- *
186
- * @returns Metadata change info
187
- */
188
- export function getWorkflowMetadataChanged() {
189
- const metadata = getWorkflowMetadata();
190
- return {
191
- createdAt: metadata.createdAt,
192
- updatedAt: metadata.updatedAt,
193
- version: metadata.version ?? '1.0.0'
194
- };
195
- }
196
- /**
197
- * Get connected handles reactively
198
- *
199
- * Provides a Set of all handle IDs that are currently connected to edges.
200
- * Used by node components to implement hideUnconnectedHandles functionality.
201
- *
202
- * @example
203
- * ```typescript
204
- * import { getConnectedHandles } from './workflowStore.svelte.js';
205
- *
206
- * // Check if a specific handle is connected
207
- * const isConnected = getConnectedHandles().has('node-1-input-data');
208
- * ```
209
- */
210
- export function getConnectedHandles() {
211
- const edges = getWorkflowEdges();
212
- const handles = new Set();
213
- edges.forEach((edge) => {
214
- // Add source handle (output port)
215
- if (edge.sourceHandle) {
216
- handles.add(edge.sourceHandle);
217
- }
218
- // Add target handle (input port)
219
- if (edge.targetHandle) {
220
- handles.add(edge.targetHandle);
221
- }
222
- });
223
- return handles;
224
- }
225
- // =========================================================================
226
- // Callback Setters
227
- // =========================================================================
228
- /**
229
- * Set the dirty state change callback
230
- *
231
- * @param callback - Function to call when dirty state changes
232
- */
233
- export function setOnDirtyStateChange(callback) {
234
- onDirtyStateChangeCallback = callback;
235
- }
236
- /**
237
- * Set the workflow change callback
238
- *
239
- * @param callback - Function to call when workflow changes
240
- */
241
- export function setOnWorkflowChange(callback) {
242
- onWorkflowChangeCallback = callback;
243
- }
244
- // =========================================================================
245
- // Internal Helpers
246
- // =========================================================================
247
- /**
248
- * Bump the edit version and notify dirty state change if needed.
249
- * Called by every mutation action.
250
- */
251
- function bumpVersion() {
252
- _editVersion++;
253
- // Dirty state just flipped from clean → dirty
254
- if (_editVersion - 1 === _savedVersion) {
255
- if (onDirtyStateChangeCallback) {
256
- onDirtyStateChangeCallback(true);
257
- }
258
- }
259
- }
260
- /**
261
- * Notify external listeners of a workflow change.
262
- * Does NOT bump the version — callers that mutate must call bumpVersion() explicitly.
263
- *
264
- * @param changeType - The type of change that occurred
265
- */
266
- function notifyWorkflowChange(changeType) {
267
- if (workflowState && onWorkflowChangeCallback) {
268
- onWorkflowChangeCallback(workflowState, changeType);
269
- }
270
- }
271
- /**
272
- * Mark the current workflow state as saved.
273
- *
274
- * Captures the current edit version so isDirty becomes false.
275
- * Call this after a successful backend save.
276
- */
277
- export function markAsSaved() {
278
- const wasDirty = _editVersion !== _savedVersion;
279
- _savedVersion = _editVersion;
280
- if (wasDirty && onDirtyStateChangeCallback) {
281
- onDirtyStateChangeCallback(false);
282
- }
283
- }
284
- /**
285
- * Check if there are unsaved changes (non-reactive version for plain TS)
286
- *
287
- * @returns true if there are unsaved changes
288
- */
289
- export function isDirty() {
290
- return _editVersion !== _savedVersion;
291
- }
292
- /**
293
- * Get the current edit version.
294
- *
295
- * Use this for the save verification protocol:
296
- * 1. Before save: `const v = getEditVersion()`
297
- * 2. Include `v` in the save request payload
298
- * 3. Backend echoes `v` in the response
299
- * 4. If echoed version matches: `markAsSaved()`
300
- * 5. If not: the save didn't persist the version you submitted — reset from backend response
301
- *
302
- * @returns The current monotonic edit version
303
- */
304
- export function getEditVersion() {
305
- return _editVersion;
306
- }
307
34
  /**
308
- * Enable or disable history recording
35
+ * Normalize workflow metadata at load time (the only back-compat path we keep,
36
+ * mirroring the storage-key migration in commit 8fab9157).
309
37
  *
310
- * Useful for bulk operations where you don't want individual history entries.
38
+ * Rules:
39
+ * - missing `metadata` → `buildMetadata(undefined)` (fresh required defaults)
40
+ * - legacy `version` key present → copied to `schemaVersion` when absent, then
41
+ * the legacy key is dropped
311
42
  *
312
- * @param enabled - Whether history should be recorded
43
+ * Idempotent: re-running on an already-healed workflow returns equivalent
44
+ * metadata (round-trip stable).
313
45
  */
314
- export function setHistoryEnabled(enabled) {
315
- historyEnabled = enabled;
316
- }
317
- /**
318
- * Check if history recording is enabled
319
- *
320
- * @returns true if history is being recorded
321
- */
322
- export function isHistoryEnabled() {
323
- return historyEnabled;
324
- }
325
- /**
326
- * Set the restoring from history flag
327
- *
328
- * Used internally by the history store when performing undo/redo.
329
- *
330
- * @param restoring - Whether we're currently restoring from history
331
- */
332
- export function setRestoringFromHistory(restoring) {
333
- isRestoringFromHistory = restoring;
334
- }
335
- /**
336
- * Push current state to history before making changes
337
- *
338
- * @param description - Description of the change about to be made
339
- * @param workflow - Optional workflow to push (uses store if not provided)
340
- */
341
- function pushToHistory(description, workflow) {
342
- if (!historyEnabled || isRestoringFromHistory) {
343
- return;
344
- }
345
- const workflowToPush = workflow ?? workflowState;
346
- if (workflowToPush) {
347
- historyService.push(workflowToPush, { description });
46
+ export function normalizeWorkflowMetadata(workflow) {
47
+ const raw = workflow.metadata;
48
+ if (!raw) {
49
+ return { ...workflow, metadata: buildMetadata(undefined) };
348
50
  }
51
+ const { version: legacyVersion, ...rest } = raw;
52
+ const healed = {
53
+ ...rest,
54
+ schemaVersion: rest.schemaVersion ?? legacyVersion ?? WORKFLOW_SCHEMA_VERSION
55
+ };
56
+ return { ...workflow, metadata: healed };
349
57
  }
350
- /**
351
- * Get the current workflow (non-reactive version for plain TS)
352
- *
353
- * @returns The current workflow or null
354
- */
355
- export function getWorkflow() {
356
- return workflowState;
357
- }
358
- // =========================================================================
359
- // Helper Functions
360
- // =========================================================================
361
58
  /**
362
59
  * Check if workflow data has actually changed
363
60
  *
@@ -397,235 +94,486 @@ function hasWorkflowDataChanged(currentWorkflow, newNodes, newEdges) {
397
94
  }
398
95
  return false;
399
96
  }
97
+ /**
98
+ * Heal nodes that are missing `data.nodeId`.
99
+ *
100
+ * Node components derive their handle IDs from `data.nodeId` — a node without
101
+ * it renders with zero handles and SvelteFlow silently drops every edge
102
+ * anchored to it, making an intact graph look corrupted on the canvas.
103
+ * `data.nodeId` always equals the node's own `id`, so it is safe to restore.
104
+ */
105
+ function healMissingNodeIds(workflow) {
106
+ if (!workflow.nodes?.some((node) => !node.data.nodeId)) {
107
+ return workflow;
108
+ }
109
+ return {
110
+ ...workflow,
111
+ nodes: workflow.nodes.map((node) => node.data.nodeId ? node : { ...node, data: { ...node.data, nodeId: node.id } })
112
+ };
113
+ }
400
114
  // =========================================================================
401
- // Workflow Actions
115
+ // WorkflowStore (per-instance reactive state)
402
116
  // =========================================================================
403
117
  /**
404
- * Actions for updating the workflow
118
+ * Per-instance workflow state with dirty tracking and history integration.
405
119
  *
406
- * All actions that modify the workflow will trigger dirty state updates
407
- * and emit change events.
120
+ * All reads go through getters backed by `$state`, so reading them inside a
121
+ * component template or `$derived` tracks reactively, exactly like the
122
+ * legacy module-level functions did.
408
123
  */
409
- export const workflowActions = {
124
+ export class WorkflowStore {
125
+ /** Current workflow */
126
+ #workflow = $state(null);
127
+ /**
128
+ * Monotonic edit version — bumps on every mutation action.
129
+ *
130
+ * Used for two purposes:
131
+ * 1. O(1) dirty detection: isDirty = #editVersion !== #savedVersion
132
+ * 2. Save verification protocol: include the version in the save request,
133
+ * backend echoes it back. If the echoed version doesn't match, the save
134
+ * didn't persist the version the client submitted.
135
+ */
136
+ #editVersion = $state(0);
137
+ /**
138
+ * Edit version captured at the last successful save.
139
+ * When #editVersion === #savedVersion, the workflow is clean.
140
+ */
141
+ #savedVersion = $state(0);
142
+ /** Callback for dirty state changes — set by the App component. */
143
+ #onDirtyStateChange = null;
144
+ /** Callback for workflow changes — set by the App component. */
145
+ #onWorkflowChange = null;
146
+ /**
147
+ * Whether we're currently restoring from history (undo/redo).
148
+ * When true, prevents pushing to history to avoid recursive loops.
149
+ */
150
+ #restoringFromHistory = false;
151
+ /** Whether history recording is enabled (disable for bulk operations). */
152
+ #historyEnabled = true;
153
+ /** Undo/redo engine for this instance (constructor-injected). */
154
+ #history;
155
+ /** Bound mutation facade — see {@link WorkflowStoreActions}. */
156
+ actions;
157
+ constructor(history) {
158
+ this.#history = history;
159
+ this.actions = Object.freeze({
160
+ initialize: this.initialize.bind(this),
161
+ updateWorkflow: this.updateWorkflow.bind(this),
162
+ restoreFromHistory: this.restoreFromHistory.bind(this),
163
+ updateNodes: this.updateNodes.bind(this),
164
+ updateEdges: this.updateEdges.bind(this),
165
+ updateName: this.updateName.bind(this),
166
+ addNode: this.addNode.bind(this),
167
+ removeNode: this.removeNode.bind(this),
168
+ addEdge: this.addEdge.bind(this),
169
+ removeEdge: this.removeEdge.bind(this),
170
+ updateNode: this.updateNode.bind(this),
171
+ clear: this.clear.bind(this),
172
+ updateMetadata: this.updateMetadata.bind(this),
173
+ batchUpdate: this.batchUpdate.bind(this),
174
+ swapNode: this.swapNode.bind(this),
175
+ pushHistory: this.pushHistory.bind(this)
176
+ });
177
+ }
178
+ // -----------------------------------------------------------------------
179
+ // Reactive getters
180
+ // -----------------------------------------------------------------------
181
+ /** The current workflow, or null when none is loaded. */
182
+ get current() {
183
+ return this.#workflow;
184
+ }
185
+ /**
186
+ * Whether there are unsaved changes.
187
+ *
188
+ * Reads both #editVersion and #savedVersion, so Svelte tracks them as
189
+ * reactive dependencies — any component using this re-renders when dirty
190
+ * state changes.
191
+ */
192
+ get isDirty() {
193
+ return this.#editVersion !== this.#savedVersion;
194
+ }
195
+ /** The workflow ID, or null. */
196
+ get id() {
197
+ return this.#workflow?.id ?? null;
198
+ }
199
+ /** The workflow name, or 'Untitled Workflow'. */
200
+ get name() {
201
+ return this.#workflow?.name ?? 'Untitled Workflow';
202
+ }
203
+ /** The workflow nodes. */
204
+ get nodes() {
205
+ return this.#workflow?.nodes ?? [];
206
+ }
207
+ /** The workflow edges. */
208
+ get edges() {
209
+ return this.#workflow?.edges ?? [];
210
+ }
211
+ /** The workflow metadata, with defaults. */
212
+ get metadata() {
213
+ return (this.#workflow?.metadata ?? {
214
+ schemaVersion: WORKFLOW_SCHEMA_VERSION,
215
+ createdAt: new Date().toISOString(),
216
+ updatedAt: new Date().toISOString(),
217
+ versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
218
+ updateNumber: 0
219
+ });
220
+ }
221
+ /** The workflow format string. */
222
+ get format() {
223
+ return this.#workflow?.metadata?.format ?? DEFAULT_WORKFLOW_FORMAT;
224
+ }
225
+ /** Workflow change summary (useful for triggering saves). */
226
+ get changeSummary() {
227
+ return {
228
+ nodes: this.nodes,
229
+ edges: this.edges,
230
+ name: this.name
231
+ };
232
+ }
233
+ /** Workflow validation state. */
234
+ get validation() {
235
+ const nodes = this.nodes;
236
+ const edges = this.edges;
237
+ return {
238
+ hasNodes: nodes.length > 0,
239
+ hasEdges: edges.length > 0,
240
+ nodeCount: nodes.length,
241
+ edgeCount: edges.length,
242
+ isValid: nodes.length > 0 && edges.length >= 0
243
+ };
244
+ }
245
+ /** Workflow metadata change summary. */
246
+ get metadataChangeSummary() {
247
+ const metadata = this.metadata;
248
+ return {
249
+ createdAt: metadata.createdAt,
250
+ updatedAt: metadata.updatedAt,
251
+ schemaVersion: metadata.schemaVersion ?? WORKFLOW_SCHEMA_VERSION
252
+ };
253
+ }
254
+ /**
255
+ * Set of all handle IDs currently connected to edges.
256
+ *
257
+ * Used by node components to implement hideUnconnectedHandles.
258
+ */
259
+ get connectedHandles() {
260
+ const handles = new Set();
261
+ this.edges.forEach((edge) => {
262
+ // Add source handle (output port)
263
+ if (edge.sourceHandle) {
264
+ handles.add(edge.sourceHandle);
265
+ }
266
+ // Add target handle (input port)
267
+ if (edge.targetHandle) {
268
+ handles.add(edge.targetHandle);
269
+ }
270
+ });
271
+ return handles;
272
+ }
273
+ /**
274
+ * The current monotonic edit version.
275
+ *
276
+ * Use this for the save verification protocol:
277
+ * 1. Before save: `const v = store.editVersion`
278
+ * 2. Include `v` in the save request payload
279
+ * 3. Backend echoes `v` in the response
280
+ * 4. If echoed version matches: `store.markAsSaved()`
281
+ * 5. If not: the save didn't persist the version you submitted — reset from backend response
282
+ */
283
+ get editVersion() {
284
+ return this.#editVersion;
285
+ }
286
+ /** Whether history recording is enabled. */
287
+ get historyEnabled() {
288
+ return this.#historyEnabled;
289
+ }
290
+ /** Enable or disable history recording (e.g. for bulk operations). */
291
+ set historyEnabled(enabled) {
292
+ this.#historyEnabled = enabled;
293
+ }
294
+ // -----------------------------------------------------------------------
295
+ // Callback setters & save protocol
296
+ // -----------------------------------------------------------------------
297
+ /** Set the dirty state change callback. */
298
+ setOnDirtyStateChange(callback) {
299
+ this.#onDirtyStateChange = callback;
300
+ }
301
+ /** Set the workflow change callback. */
302
+ setOnWorkflowChange(callback) {
303
+ this.#onWorkflowChange = callback;
304
+ }
305
+ /**
306
+ * Mark the current workflow state as saved.
307
+ *
308
+ * Captures the current edit version so isDirty becomes false.
309
+ * Call this after a successful backend save.
310
+ */
311
+ markAsSaved() {
312
+ const wasDirty = this.#editVersion !== this.#savedVersion;
313
+ this.#savedVersion = this.#editVersion;
314
+ if (wasDirty && this.#onDirtyStateChange) {
315
+ this.#onDirtyStateChange(false);
316
+ }
317
+ }
318
+ /**
319
+ * Set the restoring from history flag.
320
+ *
321
+ * Used internally by the history store when performing undo/redo.
322
+ */
323
+ setRestoringFromHistory(restoring) {
324
+ this.#restoringFromHistory = restoring;
325
+ }
326
+ // -----------------------------------------------------------------------
327
+ // Internal helpers
328
+ // -----------------------------------------------------------------------
329
+ /**
330
+ * Bump the edit version and notify dirty state change if needed.
331
+ * Called by every mutation action.
332
+ */
333
+ #bumpVersion() {
334
+ this.#editVersion++;
335
+ // Dirty state just flipped from clean → dirty
336
+ if (this.#editVersion - 1 === this.#savedVersion) {
337
+ if (this.#onDirtyStateChange) {
338
+ this.#onDirtyStateChange(true);
339
+ }
340
+ }
341
+ }
342
+ /**
343
+ * Notify external listeners of a workflow change.
344
+ * Does NOT bump the version — callers that mutate must call #bumpVersion() explicitly.
345
+ */
346
+ #notifyWorkflowChange(changeType) {
347
+ if (this.#workflow && this.#onWorkflowChange) {
348
+ this.#onWorkflowChange(this.#workflow, changeType);
349
+ }
350
+ }
351
+ /**
352
+ * Push current state to history before making changes.
353
+ *
354
+ * @param description - Description of the change about to be made
355
+ * @param workflow - Optional workflow to push (uses store state if not provided)
356
+ */
357
+ #pushToHistory(description, workflow) {
358
+ if (!this.#historyEnabled || this.#restoringFromHistory) {
359
+ return;
360
+ }
361
+ const workflowToPush = workflow ?? this.#workflow;
362
+ if (workflowToPush) {
363
+ this.#history.push(workflowToPush, { description });
364
+ }
365
+ }
366
+ // -----------------------------------------------------------------------
367
+ // Mutation actions
368
+ // -----------------------------------------------------------------------
410
369
  /**
411
- * Initialize workflow (from load or new)
370
+ * Initialize workflow (from load or new).
412
371
  *
413
372
  * This sets the initial saved snapshot, clears dirty state, and initializes history.
414
373
  */
415
- initialize: (workflow) => {
416
- workflowState = workflow;
374
+ initialize(workflow) {
375
+ workflow = normalizeWorkflowMetadata(workflow);
376
+ workflow = healMissingNodeIds(workflow);
377
+ this.#workflow = workflow;
417
378
  // Reset version counters — workflow is "clean" after initialization
418
- _editVersion = 0;
419
- _savedVersion = 0;
420
- if (onDirtyStateChangeCallback) {
421
- onDirtyStateChangeCallback(false);
379
+ this.#editVersion = 0;
380
+ this.#savedVersion = 0;
381
+ if (this.#onDirtyStateChange) {
382
+ this.#onDirtyStateChange(false);
422
383
  }
423
384
  // Initialize history with the loaded workflow
424
- historyService.initialize(workflow);
425
- },
385
+ this.#history.initialize(workflow);
386
+ }
426
387
  /**
427
- * Update the entire workflow
388
+ * Update the entire workflow.
428
389
  *
429
390
  * Note: This is typically called from SvelteFlow sync and should not push to history
430
391
  * for every small change. History is pushed by specific actions or drag handlers.
431
392
  */
432
- updateWorkflow: (workflow) => {
433
- workflowState = workflow;
434
- bumpVersion();
435
- notifyWorkflowChange('metadata');
436
- },
393
+ updateWorkflow(workflow) {
394
+ this.#workflow = workflow;
395
+ this.#bumpVersion();
396
+ this.#notifyWorkflowChange('metadata');
397
+ }
437
398
  /**
438
- * Restore workflow from history (undo/redo)
399
+ * Restore workflow from history (undo/redo).
439
400
  *
440
401
  * This bypasses history recording to prevent recursive loops.
441
402
  */
442
- restoreFromHistory: (workflow) => {
443
- isRestoringFromHistory = true;
444
- workflowState = workflow;
445
- bumpVersion();
446
- notifyWorkflowChange('metadata');
447
- isRestoringFromHistory = false;
448
- },
449
- /**
450
- * Update nodes
451
- */
452
- updateNodes: (nodes) => {
453
- if (!workflowState)
403
+ restoreFromHistory(workflow) {
404
+ this.#restoringFromHistory = true;
405
+ workflow = normalizeWorkflowMetadata(workflow);
406
+ this.#workflow = workflow;
407
+ this.#bumpVersion();
408
+ this.#notifyWorkflowChange('metadata');
409
+ this.#restoringFromHistory = false;
410
+ }
411
+ /** Update nodes. */
412
+ updateNodes(nodes) {
413
+ if (!this.#workflow)
454
414
  return;
455
415
  // Check if nodes have actually changed to prevent infinite loops
456
- if (!hasWorkflowDataChanged(workflowState, nodes, workflowState.edges)) {
416
+ if (!hasWorkflowDataChanged(this.#workflow, nodes, this.#workflow.edges)) {
457
417
  return;
458
418
  }
459
419
  // Generate unique version identifier
460
420
  const versionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
461
- workflowState = {
462
- ...workflowState,
421
+ this.#workflow = {
422
+ ...this.#workflow,
463
423
  nodes,
464
- metadata: buildMetadata(workflowState.metadata, {
424
+ metadata: buildMetadata(this.#workflow.metadata, {
465
425
  versionId,
466
- updateNumber: (workflowState.metadata?.updateNumber ?? 0) + 1
426
+ updateNumber: (this.#workflow.metadata?.updateNumber ?? 0) + 1
467
427
  })
468
428
  };
469
- bumpVersion();
470
- notifyWorkflowChange('node_move');
471
- },
472
- /**
473
- * Update edges
474
- */
475
- updateEdges: (edges) => {
476
- if (!workflowState)
429
+ this.#bumpVersion();
430
+ this.#notifyWorkflowChange('node_move');
431
+ }
432
+ /** Update edges. */
433
+ updateEdges(edges) {
434
+ if (!this.#workflow)
477
435
  return;
478
436
  // Check if edges have actually changed to prevent infinite loops
479
- if (!hasWorkflowDataChanged(workflowState, workflowState.nodes, edges)) {
437
+ if (!hasWorkflowDataChanged(this.#workflow, this.#workflow.nodes, edges)) {
480
438
  return;
481
439
  }
482
440
  // Generate unique version identifier
483
441
  const versionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
484
- workflowState = {
485
- ...workflowState,
442
+ this.#workflow = {
443
+ ...this.#workflow,
486
444
  edges,
487
- metadata: buildMetadata(workflowState.metadata, {
445
+ metadata: buildMetadata(this.#workflow.metadata, {
488
446
  versionId,
489
- updateNumber: (workflowState.metadata?.updateNumber ?? 0) + 1
447
+ updateNumber: (this.#workflow.metadata?.updateNumber ?? 0) + 1
490
448
  })
491
449
  };
492
- bumpVersion();
493
- notifyWorkflowChange('edge_add');
494
- },
495
- /**
496
- * Update workflow name
497
- */
498
- updateName: (name) => {
499
- if (!workflowState)
450
+ this.#bumpVersion();
451
+ this.#notifyWorkflowChange('edge_add');
452
+ }
453
+ /** Update workflow name. */
454
+ updateName(name) {
455
+ if (!this.#workflow)
500
456
  return;
501
- workflowState = {
502
- ...workflowState,
457
+ this.#workflow = {
458
+ ...this.#workflow,
503
459
  name,
504
- metadata: buildMetadata(workflowState.metadata)
460
+ metadata: buildMetadata(this.#workflow.metadata)
505
461
  };
506
- bumpVersion();
507
- notifyWorkflowChange('name');
508
- },
509
- /**
510
- * Add a node
511
- */
512
- addNode: (node) => {
513
- pushToHistory('Add node');
514
- if (!workflowState)
462
+ this.#bumpVersion();
463
+ this.#notifyWorkflowChange('name');
464
+ }
465
+ /** Add a node. */
466
+ addNode(node) {
467
+ this.#pushToHistory('Add node');
468
+ if (!this.#workflow)
515
469
  return;
516
- workflowState = {
517
- ...workflowState,
518
- nodes: [...workflowState.nodes, node],
519
- metadata: buildMetadata(workflowState.metadata)
470
+ this.#workflow = {
471
+ ...this.#workflow,
472
+ nodes: [...this.#workflow.nodes, node],
473
+ metadata: buildMetadata(this.#workflow.metadata)
520
474
  };
521
- bumpVersion();
522
- notifyWorkflowChange('node_add');
523
- },
475
+ this.#bumpVersion();
476
+ this.#notifyWorkflowChange('node_add');
477
+ }
524
478
  /**
525
- * Remove a node
479
+ * Remove a node.
526
480
  *
527
481
  * This is an atomic operation that also removes connected edges.
528
482
  * A single undo will restore both the node and its edges.
529
483
  */
530
- removeNode: (nodeId) => {
531
- pushToHistory('Delete node');
532
- if (!workflowState)
484
+ removeNode(nodeId) {
485
+ this.#pushToHistory('Delete node');
486
+ if (!this.#workflow)
533
487
  return;
534
- workflowState = {
535
- ...workflowState,
536
- nodes: workflowState.nodes.filter((node) => node.id !== nodeId),
537
- edges: workflowState.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId),
538
- metadata: buildMetadata(workflowState.metadata)
488
+ this.#workflow = {
489
+ ...this.#workflow,
490
+ nodes: this.#workflow.nodes.filter((node) => node.id !== nodeId),
491
+ edges: this.#workflow.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId),
492
+ metadata: buildMetadata(this.#workflow.metadata)
539
493
  };
540
- bumpVersion();
541
- notifyWorkflowChange('node_remove');
542
- },
543
- /**
544
- * Add an edge
545
- */
546
- addEdge: (edge) => {
547
- pushToHistory('Add connection');
548
- if (!workflowState)
494
+ this.#bumpVersion();
495
+ this.#notifyWorkflowChange('node_remove');
496
+ }
497
+ /** Add an edge. */
498
+ addEdge(edge) {
499
+ this.#pushToHistory('Add connection');
500
+ if (!this.#workflow)
549
501
  return;
550
- workflowState = {
551
- ...workflowState,
552
- edges: [...workflowState.edges, edge],
553
- metadata: buildMetadata(workflowState.metadata)
502
+ this.#workflow = {
503
+ ...this.#workflow,
504
+ edges: [...this.#workflow.edges, edge],
505
+ metadata: buildMetadata(this.#workflow.metadata)
554
506
  };
555
- bumpVersion();
556
- notifyWorkflowChange('edge_add');
557
- },
558
- /**
559
- * Remove an edge
560
- */
561
- removeEdge: (edgeId) => {
562
- pushToHistory('Delete connection');
563
- if (!workflowState)
507
+ this.#bumpVersion();
508
+ this.#notifyWorkflowChange('edge_add');
509
+ }
510
+ /** Remove an edge. */
511
+ removeEdge(edgeId) {
512
+ this.#pushToHistory('Delete connection');
513
+ if (!this.#workflow)
564
514
  return;
565
- workflowState = {
566
- ...workflowState,
567
- edges: workflowState.edges.filter((edge) => edge.id !== edgeId),
568
- metadata: buildMetadata(workflowState.metadata)
515
+ this.#workflow = {
516
+ ...this.#workflow,
517
+ edges: this.#workflow.edges.filter((edge) => edge.id !== edgeId),
518
+ metadata: buildMetadata(this.#workflow.metadata)
569
519
  };
570
- bumpVersion();
571
- notifyWorkflowChange('edge_remove');
572
- },
520
+ this.#bumpVersion();
521
+ this.#notifyWorkflowChange('edge_remove');
522
+ }
573
523
  /**
574
- * Update a specific node
524
+ * Update a specific node.
575
525
  *
576
526
  * Used for config changes. Pushes to history for undo support.
577
527
  */
578
- updateNode: (nodeId, updates) => {
579
- pushToHistory('Update node config');
580
- if (!workflowState)
528
+ updateNode(nodeId, updates) {
529
+ this.#pushToHistory('Update node config');
530
+ if (!this.#workflow)
581
531
  return;
582
- workflowState = {
583
- ...workflowState,
584
- nodes: workflowState.nodes.map((node) => node.id === nodeId ? { ...node, ...updates } : node),
585
- metadata: buildMetadata(workflowState.metadata)
532
+ this.#workflow = {
533
+ ...this.#workflow,
534
+ nodes: this.#workflow.nodes.map((node) => node.id === nodeId ? { ...node, ...updates } : node),
535
+ metadata: buildMetadata(this.#workflow.metadata)
586
536
  };
587
- bumpVersion();
588
- notifyWorkflowChange('node_config');
589
- },
537
+ this.#bumpVersion();
538
+ this.#notifyWorkflowChange('node_config');
539
+ }
590
540
  /**
591
- * Clear the workflow
541
+ * Clear the workflow.
592
542
  *
593
543
  * Resets the workflow and clears history.
594
544
  */
595
- clear: () => {
596
- workflowState = null;
597
- _editVersion = 0;
598
- _savedVersion = 0;
599
- historyService.clear();
600
- if (onDirtyStateChangeCallback) {
601
- onDirtyStateChangeCallback(false);
545
+ clear() {
546
+ this.#workflow = null;
547
+ this.#editVersion = 0;
548
+ this.#savedVersion = 0;
549
+ this.#history.clear();
550
+ if (this.#onDirtyStateChange) {
551
+ this.#onDirtyStateChange(false);
602
552
  }
603
- },
604
- /**
605
- * Update workflow metadata
606
- */
607
- updateMetadata: (metadata) => {
608
- if (!workflowState)
553
+ }
554
+ /** Update workflow metadata. */
555
+ updateMetadata(metadata) {
556
+ if (!this.#workflow)
609
557
  return;
610
- workflowState = {
611
- ...workflowState,
612
- metadata: buildMetadata(workflowState.metadata, metadata)
558
+ this.#workflow = {
559
+ ...this.#workflow,
560
+ metadata: buildMetadata(this.#workflow.metadata, metadata)
613
561
  };
614
- bumpVersion();
615
- notifyWorkflowChange('metadata');
616
- },
562
+ this.#bumpVersion();
563
+ this.#notifyWorkflowChange('metadata');
564
+ }
617
565
  /**
618
- * Batch update nodes and edges
566
+ * Batch update nodes and edges.
619
567
  *
620
568
  * Useful for complex operations that update multiple things at once.
621
569
  * Creates a single history entry for the entire batch.
622
570
  */
623
- batchUpdate: (updates) => {
624
- pushToHistory('Batch update');
625
- if (!workflowState)
571
+ batchUpdate(updates) {
572
+ this.#pushToHistory('Batch update');
573
+ if (!this.#workflow)
626
574
  return;
627
- workflowState = {
628
- ...workflowState,
575
+ this.#workflow = {
576
+ ...this.#workflow,
629
577
  ...(updates.nodes && { nodes: updates.nodes }),
630
578
  ...(updates.edges && { edges: updates.edges }),
631
579
  ...(updates.name && { name: updates.name }),
@@ -633,32 +581,32 @@ export const workflowActions = {
633
581
  description: updates.description
634
582
  }),
635
583
  ...(updates.config !== undefined && { config: updates.config }),
636
- metadata: buildMetadata(workflowState.metadata, updates.metadata ?? undefined)
584
+ metadata: buildMetadata(this.#workflow.metadata, updates.metadata ?? undefined)
637
585
  };
638
- bumpVersion();
639
- notifyWorkflowChange('metadata');
640
- },
586
+ this.#bumpVersion();
587
+ this.#notifyWorkflowChange('metadata');
588
+ }
641
589
  /**
642
590
  * Swap a node — atomically replaces nodes and edges with a descriptive history entry.
643
591
  *
644
592
  * Unlike batchUpdate, this uses `"node_swap"` as the change type and
645
593
  * records a meaningful description for the undo history.
646
594
  */
647
- swapNode: (updates) => {
648
- pushToHistory(updates.description ?? 'Swap node');
649
- if (!workflowState)
595
+ swapNode(updates) {
596
+ this.#pushToHistory(updates.description ?? 'Swap node');
597
+ if (!this.#workflow)
650
598
  return;
651
- workflowState = {
652
- ...workflowState,
599
+ this.#workflow = {
600
+ ...this.#workflow,
653
601
  nodes: updates.nodes,
654
602
  edges: updates.edges,
655
- metadata: buildMetadata(workflowState.metadata)
603
+ metadata: buildMetadata(this.#workflow.metadata)
656
604
  };
657
- bumpVersion();
658
- notifyWorkflowChange('node_swap');
659
- },
605
+ this.#bumpVersion();
606
+ this.#notifyWorkflowChange('node_swap');
607
+ }
660
608
  /**
661
- * Push current state to history manually
609
+ * Push current state to history manually.
662
610
  *
663
611
  * Use this before operations that modify the workflow through other means
664
612
  * (e.g., drag operations handled by SvelteFlow directly).
@@ -666,7 +614,7 @@ export const workflowActions = {
666
614
  * @param description - Description of the upcoming change
667
615
  * @param workflow - Optional workflow to push (uses store state if not provided)
668
616
  */
669
- pushHistory: (description, workflow) => {
670
- pushToHistory(description, workflow);
617
+ pushHistory(description, workflow) {
618
+ this.#pushToHistory(description, workflow);
671
619
  }
672
- };
620
+ }