@kiberon-labs/behave-graph-flow 1.0.0 → 2.0.0

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 (314) hide show
  1. package/.fallowrc.json +16 -0
  2. package/.storybook/main.ts +32 -0
  3. package/.storybook/preview.ts +16 -0
  4. package/.storybook/styles.css +10 -0
  5. package/.storybook/vscode.css +814 -0
  6. package/.turbo/turbo-build.log +7 -0
  7. package/LICENSE +6 -0
  8. package/README.md +2 -2
  9. package/data/Polynomial.json +510 -0
  10. package/data/sequence.json +337 -0
  11. package/data/trigger-event.json +241 -0
  12. package/data/variable-change.json +210 -0
  13. package/dist/entry.css +4 -0
  14. package/dist/index.css +39 -0
  15. package/dist/index.css.map +1 -0
  16. package/dist/index.d.ts +2282 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +14873 -0
  19. package/dist/index.js.map +1 -0
  20. package/docs/notifications.md +246 -0
  21. package/docs/protocol.md +679 -0
  22. package/docs/specifics.md +191 -0
  23. package/package.json +85 -21
  24. package/postcss.config.ts +3 -4
  25. package/src/annotations/index.ts +32 -0
  26. package/src/components/FloatingToolbar/index.module.css +45 -0
  27. package/src/components/FloatingToolbar/index.tsx +256 -0
  28. package/src/components/Flow.tsx +276 -75
  29. package/src/components/contextMenus/NodePicker.module.css +274 -0
  30. package/src/components/contextMenus/NodePicker.tsx +481 -0
  31. package/src/components/contextMenus/edge.tsx +108 -0
  32. package/src/components/contextMenus/node.tsx +155 -0
  33. package/src/components/contextMenus/selection.tsx +77 -0
  34. package/src/components/controls/any/index.tsx +8 -0
  35. package/src/components/controls/boolean/index.tsx +13 -0
  36. package/src/components/controls/colorPicker/InputPopover.module.css +100 -0
  37. package/src/components/controls/colorPicker/InputPopover.tsx +31 -0
  38. package/src/components/controls/colorPicker/index.module.css +18 -0
  39. package/src/components/controls/colorPicker/index.tsx +61 -0
  40. package/src/components/controls/number/index.tsx +35 -0
  41. package/src/components/controls/string/index.tsx +16 -0
  42. package/src/components/edges/index.tsx +469 -0
  43. package/src/components/edges/offsetBezier.ts +134 -0
  44. package/src/components/hotKeys.tsx +20 -0
  45. package/src/components/layoutController/index.module.css +10 -0
  46. package/src/components/layoutController/index.tsx +117 -0
  47. package/src/components/layoutController/utils.ts +205 -0
  48. package/src/components/menubar/defaults.tsx +480 -0
  49. package/src/components/menubar/index.tsx +49 -0
  50. package/src/components/menubar/menuItem.module.css +16 -0
  51. package/src/components/menubar/menuItem.tsx +32 -0
  52. package/src/components/nodes/behave/Node.module.css +23 -0
  53. package/src/components/nodes/behave/Node.tsx +176 -0
  54. package/src/components/nodes/behave/NodeContainer.module.css +87 -0
  55. package/src/components/nodes/behave/NodeContainer.tsx +46 -0
  56. package/src/components/nodes/behave/index.tsx +14 -0
  57. package/src/components/nodes/comment/FormatToolbar.tsx +118 -0
  58. package/src/components/nodes/comment/comment.tsx +103 -0
  59. package/src/components/nodes/comment/styles.module.css +150 -0
  60. package/src/components/nodes/group/index.tsx +109 -0
  61. package/src/components/nodes/wrapper/index.tsx +73 -0
  62. package/src/components/nodes/wrapper/styles.module.css +113 -0
  63. package/src/components/notifications/NotificationProvider.tsx +81 -0
  64. package/src/components/notifications/index.ts +2 -0
  65. package/src/components/notifications/utils.ts +71 -0
  66. package/src/components/panels/alignment/index.module.css +20 -0
  67. package/src/components/panels/alignment/index.tsx +244 -0
  68. package/src/components/panels/base/index.tsx +5 -0
  69. package/src/components/panels/base/styles.module.css +12 -0
  70. package/src/components/panels/conversation/index.module.css +151 -0
  71. package/src/components/panels/conversation/index.tsx +162 -0
  72. package/src/components/panels/events/CustomEventsEditor.tsx +384 -0
  73. package/src/components/panels/events/EditEventPanel.tsx +315 -0
  74. package/src/components/panels/events/ManageEventsPanel.tsx +98 -0
  75. package/src/components/panels/events/index.tsx +23 -0
  76. package/src/components/panels/events/styles.module.css +236 -0
  77. package/src/components/panels/history/index.tsx +92 -0
  78. package/src/components/panels/history/styles.module.css +106 -0
  79. package/src/components/panels/keymaps/index.module.css +78 -0
  80. package/src/components/panels/keymaps/index.tsx +167 -0
  81. package/src/components/panels/layers/index.tsx +240 -0
  82. package/src/components/panels/layers/styles.module.css +110 -0
  83. package/src/components/panels/legend/index.module.css +6 -0
  84. package/src/components/panels/legend/index.tsx +76 -0
  85. package/src/components/panels/logs/index.module.css +212 -0
  86. package/src/components/panels/logs/index.tsx +288 -0
  87. package/src/components/panels/nodeInputs/InputControl.tsx +63 -0
  88. package/src/components/panels/nodeInputs/InputsGroup.tsx +64 -0
  89. package/src/components/panels/nodeInputs/MultipleNodesView.tsx +37 -0
  90. package/src/components/panels/nodeInputs/NodeSettings.tsx +92 -0
  91. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +125 -0
  92. package/src/components/panels/nodeInputs/OutputsGroup.tsx +65 -0
  93. package/src/components/panels/nodeInputs/SocketGenerators.tsx +32 -0
  94. package/src/components/panels/nodeInputs/index.module.css +284 -0
  95. package/src/components/panels/nodeInputs/index.tsx +339 -0
  96. package/src/components/panels/nodeInputs/useNodeHandlers.ts +76 -0
  97. package/src/components/panels/nodeInputs/useNodeInputsData.ts +173 -0
  98. package/src/components/panels/nodePicker/index.tsx +115 -0
  99. package/src/components/panels/panel/index.module.css +66 -0
  100. package/src/components/panels/panel/index.tsx +88 -0
  101. package/src/components/panels/search/index.module.css +66 -0
  102. package/src/components/panels/search/index.tsx +215 -0
  103. package/src/components/panels/systemSettings/index.tsx +206 -0
  104. package/src/components/panels/systemSettings/styles.module.css +11 -0
  105. package/src/components/panels/traces/GridLines.tsx +38 -0
  106. package/src/components/panels/traces/TimeGrid.tsx +48 -0
  107. package/src/components/panels/traces/TraceLane.tsx +62 -0
  108. package/src/components/panels/traces/TraceTooltip.tsx +22 -0
  109. package/src/components/panels/traces/TracesHeader.tsx +56 -0
  110. package/src/components/panels/traces/index.module.css +166 -0
  111. package/src/components/panels/traces/index.tsx +294 -0
  112. package/src/components/panels/traces/types.ts +48 -0
  113. package/src/components/panels/traces/useDerivedSpans.ts +212 -0
  114. package/src/components/panels/traces/utils.ts +25 -0
  115. package/src/components/panels/variables/CreateVariableScreen.tsx +162 -0
  116. package/src/components/panels/variables/ManageVariablesScreen.tsx +144 -0
  117. package/src/components/panels/variables/index.tsx +125 -0
  118. package/src/components/panels/variables/styles.module.css +236 -0
  119. package/src/components/primitives/icon.module.css +45 -0
  120. package/src/components/primitives/icon.tsx +38 -0
  121. package/src/components/sockets/input/index.tsx +76 -0
  122. package/src/components/sockets/input/styles.module.css +27 -0
  123. package/src/components/sockets/output/index.tsx +61 -0
  124. package/src/components/sockets/output/styles.module.css +27 -0
  125. package/src/css/prosemirror.css +57 -0
  126. package/src/css/rc-dock.css +112 -0
  127. package/src/css/rc-menu.css +100 -0
  128. package/src/css/vars.css +14 -0
  129. package/src/css/vscode.css +13 -0
  130. package/src/entry.css +4 -0
  131. package/src/generators/CustomEventOnTriggeredGenerator.tsx +85 -0
  132. package/src/generators/SequenceGenerator.tsx +104 -0
  133. package/src/generators/SwitchOnIntegerGenerator.tsx +256 -0
  134. package/src/generators/SwitchOnStringGenerator.tsx +263 -0
  135. package/src/generators/registerDefaultGenerators.ts +34 -0
  136. package/src/hooks/useBehaveGraphFlow.ts +17 -16
  137. package/src/hooks/useDetachNodes.ts +39 -0
  138. package/src/hooks/useFlowHandlers.ts +115 -29
  139. package/src/hooks/useWasdPan.ts +188 -0
  140. package/src/index.css +146 -0
  141. package/src/index.ts +36 -18
  142. package/src/layout/dagre.tsx +119 -0
  143. package/src/layout/elk.ts +200 -0
  144. package/src/plugin/alignment/index.ts +81 -0
  145. package/src/plugin/docs/index.tsx +299 -0
  146. package/src/plugin/docs/panel/index.tsx +200 -0
  147. package/src/plugin/docs/panel/styles.module.css +174 -0
  148. package/src/plugin/graphrunner/actions.ts +253 -0
  149. package/src/plugin/graphrunner/buttons.tsx +87 -0
  150. package/src/plugin/graphrunner/client.ts +704 -0
  151. package/src/plugin/graphrunner/index.tsx +255 -0
  152. package/src/plugin/graphrunner/panel.tsx +386 -0
  153. package/src/plugin/graphrunner/runner.ts +358 -0
  154. package/src/plugin/graphrunner/session.ts +243 -0
  155. package/src/plugin/graphrunner/store.ts +206 -0
  156. package/src/plugin/graphrunner/styles.module.css +211 -0
  157. package/src/plugin/graphrunner/transport.ts +224 -0
  158. package/src/plugin/graphrunner/types.ts +672 -0
  159. package/src/plugin/graphrunner-local/execution-utils.ts +457 -0
  160. package/src/plugin/graphrunner-local/index.tsx +166 -0
  161. package/src/plugin/graphrunner-local/panel.tsx +231 -0
  162. package/src/plugin/graphrunner-local/store.ts +41 -0
  163. package/src/plugin/graphrunner-local/styles.module.css +101 -0
  164. package/src/plugin/graphrunner-local/transport.ts +1372 -0
  165. package/src/plugin/graphrunner-local/types.ts +10 -0
  166. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +633 -0
  167. package/src/plugin/graphrunner-webworker/index.tsx +146 -0
  168. package/src/plugin/graphrunner-webworker/panel.tsx +173 -0
  169. package/src/plugin/graphrunner-webworker/store.ts +89 -0
  170. package/src/plugin/graphrunner-webworker/types.ts +17 -0
  171. package/src/plugin/graphrunner-webworker/worker-transport.ts +123 -0
  172. package/src/plugin/realtime/realtimeRunner.ts +570 -0
  173. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +92 -0
  174. package/src/specifics/CustomEventTriggerSpecific.tsx +141 -0
  175. package/src/specifics/VariableGetSpecific.tsx +110 -0
  176. package/src/specifics/VariableSetSpecific.tsx +110 -0
  177. package/src/specifics/registerDefaultSpecifics.ts +5 -0
  178. package/src/store/actions.tsx +698 -0
  179. package/src/store/chat.ts +73 -0
  180. package/src/store/controls.tsx +62 -0
  181. package/src/store/documentation.tsx +69 -0
  182. package/src/store/events.tsx +116 -0
  183. package/src/store/flow.tsx +245 -0
  184. package/src/store/graphRunnerClient.ts +110 -0
  185. package/src/store/hotKeys.tsx +323 -0
  186. package/src/store/layers.ts +259 -0
  187. package/src/store/legend.tsx +76 -0
  188. package/src/store/logs.ts +28 -0
  189. package/src/store/menubar.ts +41 -0
  190. package/src/store/refs.ts +84 -0
  191. package/src/store/registry.ts +43 -0
  192. package/src/store/selection.ts +22 -0
  193. package/src/store/settings.ts +99 -0
  194. package/src/store/socketGenerator.tsx +54 -0
  195. package/src/store/specific.tsx +75 -0
  196. package/src/store/specs.tsx +35 -0
  197. package/src/store/tabs.ts +278 -0
  198. package/src/store/toolbar.tsx +45 -0
  199. package/src/store/traces.ts +240 -0
  200. package/src/store/variables.ts +37 -0
  201. package/src/system/graph.ts +134 -0
  202. package/src/system/index.ts +3 -0
  203. package/src/system/notifications.ts +98 -0
  204. package/src/system/plugin.ts +27 -0
  205. package/src/system/provider.tsx +22 -0
  206. package/src/system/pubsub.ts +323 -0
  207. package/src/system/system.ts +223 -0
  208. package/src/system/tabLoader.tsx +265 -0
  209. package/src/system/undoRedo.ts +103 -0
  210. package/src/transformers/Uigraph.ts +60 -0
  211. package/src/transformers/behaveToFlow.ts +16 -4
  212. package/src/transformers/flowToBehave.ts +32 -12
  213. package/src/types/NodeMetadata.ts +27 -0
  214. package/src/types/graph.ts +49 -0
  215. package/src/types/nodes.ts +45 -0
  216. package/src/types.ts +16 -0
  217. package/src/util/colors.ts +1 -29
  218. package/src/util/downloadJson.ts +18 -0
  219. package/src/util/extractNodeMetadata.ts +16 -0
  220. package/src/util/getPickerFilters.ts +1 -1
  221. package/src/util/isBehaveNode.ts +6 -0
  222. package/src/util/isValidConnection.ts +28 -15
  223. package/src/util/mergeSockets.ts +29 -0
  224. package/src/util/serializeVariables.ts +66 -0
  225. package/src/util/sockets.ts +43 -0
  226. package/stories/apex/layoutController/example-graph.worker.ts +39 -0
  227. package/stories/apex/layoutController/index.stories.tsx +48 -0
  228. package/stories/apex/layoutController/webworker.stories.tsx +103 -0
  229. package/stories/apex/menubar/menubar.stories.tsx +19 -0
  230. package/stories/components/colorpicker/index.stories.tsx +20 -0
  231. package/stories/components/contextMenus/edge.stories.tsx +32 -0
  232. package/stories/components/contextMenus/node.stories.tsx +26 -0
  233. package/stories/components/contextMenus/nodePicker.stories.tsx +115 -0
  234. package/stories/components/controls/any/index.stories.tsx +19 -0
  235. package/stories/components/controls/boolean/index.stories.tsx +19 -0
  236. package/stories/components/controls/colorPicker/index.stories.tsx +49 -0
  237. package/stories/components/controls/number/index.stories.tsx +19 -0
  238. package/stories/components/controls/string/index.stories.tsx +19 -0
  239. package/stories/components/nodes/behaveNode.stories.tsx +108 -0
  240. package/stories/components/nodes/comment.stories.tsx +106 -0
  241. package/stories/components/panels/alignment.stories.tsx +24 -0
  242. package/stories/components/panels/events.stories.tsx +38 -0
  243. package/stories/components/panels/graphRunner.stories.tsx +317 -0
  244. package/stories/components/panels/history.stories.tsx +37 -0
  245. package/stories/components/panels/keymaps.stories.tsx +21 -0
  246. package/stories/components/panels/legend.stories.tsx +37 -0
  247. package/stories/components/panels/logs.stories.tsx +24 -0
  248. package/stories/components/panels/nodeInputs.stories.tsx +21 -0
  249. package/stories/components/panels/nodePicker.stories.tsx +37 -0
  250. package/stories/components/panels/panel.stories.tsx +39 -0
  251. package/stories/components/panels/search.stories.tsx +24 -0
  252. package/stories/components/panels/systemSettings.stories.tsx +26 -0
  253. package/stories/components/panels/traces.stories.tsx +225 -0
  254. package/stories/components/panels/variables.stories.tsx +24 -0
  255. package/stories/defaults/defaultStoryProvider.tsx +167 -0
  256. package/stories/defaults/systemGenerator.ts +38 -0
  257. package/tests/components/edges/offsetBezier.test.ts +51 -0
  258. package/tests/components/layoutController/utils.test.ts +68 -0
  259. package/tests/components/panels/traces/utils.test.ts +52 -0
  260. package/tests/flowToBehave.test.ts +26 -4
  261. package/tests/notifications.test.ts +87 -0
  262. package/tests/saveLoad.test.ts +372 -0
  263. package/tests/util/calculateNewEdge.test.ts +98 -0
  264. package/tests/util/getSocketsByNodeTypeAndHandleType.test.ts +31 -0
  265. package/tests/util/hasPositionMetaData.test.ts +33 -0
  266. package/tests/util/isBehaveNode.test.ts +22 -0
  267. package/tests/util/isHandleConnected.test.ts +37 -0
  268. package/tests/util/mergeSockets.test.ts +43 -0
  269. package/tests/visual/README.md +64 -0
  270. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-alignment-chromium-win32.png +0 -0
  271. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  272. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  273. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  274. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  275. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  276. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  277. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  278. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  279. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  280. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  281. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  282. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  283. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-traces-chromium-win32.png +0 -0
  284. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  285. package/tests/visual/panels.visual.test.tsx +76 -0
  286. package/tsconfig.base.json +39 -0
  287. package/tsconfig.json +18 -59
  288. package/tsconfig.prod.json +23 -0
  289. package/tsdown.config.ts +15 -3
  290. package/typedoc.json +7 -7
  291. package/vite.config.js +7 -0
  292. package/vitest.config.ts +5 -2
  293. package/vitest.visual.config.ts +48 -0
  294. package/src/components/AutoSizeInput.tsx +0 -65
  295. package/src/components/Controls.tsx +0 -87
  296. package/src/components/InputSocket.tsx +0 -142
  297. package/src/components/Node.tsx +0 -68
  298. package/src/components/NodeContainer.tsx +0 -46
  299. package/src/components/NodePicker.tsx +0 -77
  300. package/src/components/OutputSocket.tsx +0 -58
  301. package/src/components/modals/ClearModal.tsx +0 -40
  302. package/src/components/modals/HelpModal.tsx +0 -36
  303. package/src/components/modals/LoadModal.tsx +0 -96
  304. package/src/components/modals/Modal.tsx +0 -64
  305. package/src/components/modals/SaveModal.tsx +0 -60
  306. package/src/hooks/useCustomNodeTypes.tsx +0 -31
  307. package/src/hooks/useGraphRunner.ts +0 -104
  308. package/src/hooks/useMergeMap.ts +0 -14
  309. package/src/hooks/useNodeSpecJson.ts +0 -20
  310. package/src/hooks/useQueriableDefinitions.ts +0 -22
  311. package/src/styles.css +0 -8
  312. package/tailwind.config.ts +0 -19
  313. package/tests/tsconfig.json +0 -10
  314. /package/src/{types.d.ts → types-declarations.d.ts} +0 -0
@@ -0,0 +1,1372 @@
1
+ /**
2
+ * Local (in-browser) transport implementation for graph execution
3
+ * Executes graphs directly using the local Engine instead of a remote server
4
+ */
5
+
6
+ import type {
7
+ GraphRunnerMessage,
8
+ RunStatus,
9
+ GraphRunnerCapabilities,
10
+ ServerVariable,
11
+ ServerEvent,
12
+ ServerGraphRunnerMessage,
13
+ RunGraphMessage,
14
+ HelloMessage,
15
+ CreateSessionMessage,
16
+ GetNodeTypesMessage,
17
+ GetStatusMessage,
18
+ CloseSessionMessage,
19
+ StopGraphMessage,
20
+ GetSocketConstraintsMessage,
21
+ AddNodeMessage,
22
+ RemoveNodeMessage,
23
+ UpdateSocketValueMessage,
24
+ UpdateNodeParamMessage,
25
+ CreateLinkMessage,
26
+ RemoveLinkMessage,
27
+ DirectExecuteNodeMessage
28
+ } from '../graphrunner/types.js';
29
+ import type { ITransport, TransportState } from '../graphrunner/transport.js';
30
+ import {
31
+ Engine,
32
+ type GraphInstance,
33
+ type ILifecycleEventEmitter,
34
+ readGraphFromJSON,
35
+ validateGraph,
36
+ ManualLifecycleEventEmitter,
37
+ DefaultLogger,
38
+ type ILogger,
39
+ Link,
40
+ makeGraphApi
41
+ } from '@kiberon-labs/behave-graph';
42
+ import type { IRegistry } from '@kiberon-labs/behave-graph';
43
+ import type { StoreApi } from 'zustand';
44
+ import type { LocalGraphRunnerStore } from './store.js';
45
+ import { sleep } from '@kiberon-labs/behave-graph';
46
+ import {
47
+ setupVariableChangeTracking,
48
+ handleGetServerVariables,
49
+ handleGetServerEvents,
50
+ handleGetSocketConstraints,
51
+ handleGetNodeTypes
52
+ } from './execution-utils.js';
53
+ import {
54
+ SessionManager,
55
+ type Session,
56
+ type SessionConfig,
57
+ type SessionFactory
58
+ } from '../graphrunner/session.js';
59
+ import { createNode } from '@kiberon-labs/behave-graph';
60
+
61
+ interface ActiveRun {
62
+ runId: string;
63
+ graphId: string;
64
+ sessionId: string;
65
+ engine: Engine;
66
+ graphInstance: GraphInstance;
67
+ registry: IRegistry;
68
+ status: RunStatus;
69
+ startedAt: number;
70
+ performance: {
71
+ nodesExecuted: number;
72
+ eventsEmitted: number;
73
+ variableChanges: number;
74
+ };
75
+ // Step execution control
76
+ isPaused: boolean;
77
+ executionPhase: 'start' | 'tick' | 'end' | 'completed';
78
+ currentTick: number;
79
+ maxTicks: number;
80
+ }
81
+
82
+ /**
83
+ * Local transport that executes graphs in the browser using the Engine
84
+ */
85
+ export class LocalTransport implements ITransport {
86
+ private state: TransportState = 'disconnected';
87
+ private messageHandlers: Array<(message: ServerGraphRunnerMessage) => void> =
88
+ [];
89
+ private stateChangeHandlers: Array<(state: TransportState) => void> = [];
90
+ private errorHandlers: Array<(error: Error) => void> = [];
91
+ private registry: IRegistry;
92
+ private sessionManager: SessionManager;
93
+ private activeRuns = new Map<string, ActiveRun>();
94
+ private store: StoreApi<LocalGraphRunnerStore> | null = null;
95
+ private variables: ServerVariable[];
96
+ private serverEvents: ServerEvent[];
97
+
98
+ constructor(
99
+ registry: IRegistry,
100
+ options?: {
101
+ store?: StoreApi<LocalGraphRunnerStore>;
102
+ variables?: ServerVariable[];
103
+ serverEvents?: ServerEvent[];
104
+ sessionFactory?: SessionFactory;
105
+ }
106
+ ) {
107
+ this.registry = registry;
108
+ this.store = options?.store ?? null;
109
+ this.variables = options?.variables ?? [];
110
+ this.serverEvents = options?.serverEvents ?? [];
111
+ this.sessionManager = new SessionManager(options?.sessionFactory);
112
+ }
113
+
114
+ /**
115
+ * Create a logger that forwards log messages to the client
116
+ */
117
+ private createTransportLogger(runId: string, graphId: string): ILogger {
118
+ const baseLogger =
119
+ (this.registry.dependencies?.ILogger as ILogger) || new DefaultLogger();
120
+
121
+ return {
122
+ log: (severity: string, text: string) => {
123
+ baseLogger.log(severity as any, text);
124
+ this.notifyMessage({
125
+ type: 'log',
126
+ runId,
127
+ graphId,
128
+ level: severity,
129
+ message: text
130
+ });
131
+ }
132
+ };
133
+ }
134
+
135
+ getState(): TransportState {
136
+ return this.state;
137
+ }
138
+
139
+ async connect(): Promise<void> {
140
+ this.setState('connected');
141
+ }
142
+
143
+ disconnect(): void {
144
+ // Clean up all active runs
145
+ for (const run of this.activeRuns.values()) {
146
+ run.engine.dispose();
147
+ }
148
+ this.activeRuns.clear();
149
+
150
+ // Close all sessions
151
+ for (const session of this.sessionManager.getActiveSessions()) {
152
+ void this.sessionManager.closeSession(session.sessionId);
153
+ }
154
+
155
+ this.setState('disconnected');
156
+ this.updateStoreActiveRuns();
157
+ }
158
+
159
+ send(message: GraphRunnerMessage): void {
160
+ // Handle messages synchronously in the browser
161
+ try {
162
+ this.handleMessage(message);
163
+ } catch (error) {
164
+ this.notifyError(
165
+ error instanceof Error ? error : new Error(String(error))
166
+ );
167
+ }
168
+ }
169
+
170
+ onMessage(handler: (message: ServerGraphRunnerMessage) => void): void {
171
+ this.messageHandlers.push(handler);
172
+ }
173
+
174
+ onStateChange(handler: (state: TransportState) => void): void {
175
+ this.stateChangeHandlers.push(handler);
176
+ }
177
+
178
+ onError(handler: (error: Error) => void): void {
179
+ this.errorHandlers.push(handler);
180
+ }
181
+
182
+ removeAllHandlers(): void {
183
+ this.messageHandlers = [];
184
+ this.stateChangeHandlers = [];
185
+ this.errorHandlers = [];
186
+ }
187
+
188
+ private setState(newState: TransportState): void {
189
+ this.state = newState;
190
+ this.stateChangeHandlers.forEach((handler) => handler(newState));
191
+ }
192
+
193
+ private notifyError(error: Error): void {
194
+ this.errorHandlers.forEach((handler) => handler(error));
195
+ }
196
+
197
+ private notifyMessage(message: ServerGraphRunnerMessage): void {
198
+ this.messageHandlers.forEach((handler) => handler(message));
199
+ }
200
+ updateStoreActiveRuns(): void {
201
+ if (this.store) {
202
+ this.store.getState().setActiveRuns(this.activeRuns.size);
203
+ }
204
+ }
205
+
206
+ private updateStoreExecutionState(
207
+ isExecuting: boolean,
208
+ isPaused: boolean
209
+ ): void {
210
+ if (this.store) {
211
+ this.store.getState().setIsExecuting(isExecuting);
212
+ this.store.getState().setIsPaused(isPaused);
213
+ }
214
+ }
215
+
216
+ private getExecutionDelay(): number {
217
+ if (this.store) {
218
+ const { stepDelay, executionSpeed } = this.store.getState();
219
+ // Apply speed multiplier and step delay
220
+ return (
221
+ stepDelay + (executionSpeed < 1.0 ? (1.0 - executionSpeed) * 100 : 0)
222
+ );
223
+ }
224
+ return 0;
225
+ }
226
+
227
+ private getExecutionStepLimit(): number {
228
+ return 1;
229
+ }
230
+
231
+ private getTickInterval(): number {
232
+ if (this.store) {
233
+ return this.store.getState().tickInterval;
234
+ }
235
+ return 50; // Default 50ms
236
+ }
237
+
238
+ /**
239
+ * Get the default sleep-based tick strategy
240
+ */
241
+ private createSleepTickStrategy(tickInterval: number): () => Promise<void> {
242
+ return async () => {
243
+ await sleep(tickInterval / 1000); // Convert ms to seconds
244
+ };
245
+ }
246
+
247
+ private generateId(prefix: string): string {
248
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
249
+ }
250
+
251
+ private handleMessage(message: GraphRunnerMessage): void {
252
+ switch (message.type) {
253
+ case 'hello':
254
+ this.handleHello(message);
255
+ break;
256
+ case 'createSession':
257
+ this.handleCreateSession(message);
258
+ break;
259
+ case 'getCapabilities':
260
+ this.handleGetCapabilities();
261
+ break;
262
+ case 'getServerVariables':
263
+ this.handleGetServerVariables(message);
264
+ break;
265
+ case 'getServerEvents':
266
+ this.handleGetServerEvents(message);
267
+ break;
268
+ case 'getSocketConstraints':
269
+ this.handleGetSocketConstraints(message);
270
+ break;
271
+ case 'getNodeTypes':
272
+ this.handleGetNodeTypes(message);
273
+ break;
274
+ case 'runGraph':
275
+ this.handleRunGraph(message);
276
+ break;
277
+ case 'stopGraph':
278
+ this.handleStopGraph(message);
279
+ break;
280
+ case 'getStatus':
281
+ this.handleGetStatus(message);
282
+ break;
283
+ case 'closeSession':
284
+ this.handleCloseSession(message);
285
+ break;
286
+ case 'addNode':
287
+ this.handleAddNode(message);
288
+ break;
289
+ case 'removeNode':
290
+ this.handleRemoveNode(message);
291
+ break;
292
+ case 'updateSocketValue':
293
+ this.handleUpdateSocketValue(message);
294
+ break;
295
+ case 'updateNodeParam':
296
+ this.handleUpdateNodeParam(message);
297
+ break;
298
+ case 'createLink':
299
+ this.handleCreateLink(message);
300
+ break;
301
+ case 'removeLink':
302
+ this.handleRemoveLink(message);
303
+ break;
304
+ case 'directExecuteNode':
305
+ this.handleDirectExecuteNode(message);
306
+ break;
307
+ default:
308
+ this.sendError(
309
+ 'PROTOCOL_VIOLATION',
310
+ `Unsupported message type: ${(message as GraphRunnerMessage).type}`
311
+ );
312
+ }
313
+ }
314
+
315
+ private handleHello(message: HelloMessage): void {
316
+ this.notifyMessage({
317
+ type: 'welcome',
318
+ protocolVersion: message.protocolVersion,
319
+ serverId: 'local-runner',
320
+ authenticated: true,
321
+ userId: 'local-user'
322
+ });
323
+ }
324
+
325
+ private handleCreateSession(message: CreateSessionMessage): void {
326
+ const sessionId = this.generateId('session');
327
+ const sessionConfig: SessionConfig = {
328
+ metadata: message.metadata
329
+ };
330
+
331
+ const session = this.sessionManager.createSession(sessionId, sessionConfig);
332
+
333
+ this.notifyMessage({
334
+ type: 'sessionCreated',
335
+ sessionId: session.sessionId,
336
+ expiresAt: session.expiresAt
337
+ });
338
+ }
339
+
340
+ private handleGetCapabilities(): void {
341
+ // Get default capabilities, can be overridden by session
342
+ const capabilities: GraphRunnerCapabilities = {
343
+ trace: true,
344
+ validation: true,
345
+ graphRegistry: false,
346
+ eventFiltering: false,
347
+ batchOperations: false,
348
+ runHistory: false,
349
+ runtimeMetadata: true,
350
+ maxConcurrentRuns: 10,
351
+ realtime: true,
352
+ maxConcurrentDynamicRuns: 10,
353
+ updateGranularity: 'socket'
354
+ };
355
+
356
+ this.notifyMessage({
357
+ type: 'capabilities',
358
+ capabilities
359
+ });
360
+ }
361
+
362
+ private handleGetServerVariables(_message: {
363
+ type: 'getServerVariables';
364
+ sessionId: string;
365
+ }): void {
366
+ handleGetServerVariables(this.variables, {
367
+ sendMessage: this.notifyMessage.bind(this),
368
+ sendError: (code, msg, details) => this.sendError(code, msg, details)
369
+ });
370
+ }
371
+
372
+ private handleGetServerEvents(_message: {
373
+ type: 'getServerEvents';
374
+ sessionId: string;
375
+ }): void {
376
+ handleGetServerEvents(this.serverEvents, {
377
+ sendMessage: this.notifyMessage.bind(this),
378
+ sendError: (code, msg, details) => this.sendError(code, msg, details)
379
+ });
380
+ }
381
+
382
+ private handleGetSocketConstraints(
383
+ message: GetSocketConstraintsMessage
384
+ ): void {
385
+ handleGetSocketConstraints(
386
+ { nodeType: message.nodeType, socketName: message.socketName },
387
+ this.registry,
388
+ {
389
+ sendMessage: this.notifyMessage.bind(this),
390
+ sendError: (code, msg, details) => this.sendError(code, msg, details)
391
+ }
392
+ );
393
+ }
394
+
395
+ private handleGetNodeTypes(_message: GetNodeTypesMessage): void {
396
+ handleGetNodeTypes(this.registry, {
397
+ sendMessage: this.notifyMessage.bind(this),
398
+ sendError: (code, msg, details) => this.sendError(code, msg, details)
399
+ });
400
+ }
401
+
402
+ private async handleRunGraph(message: RunGraphMessage): Promise<void> {
403
+ const runId = this.generateId('run');
404
+
405
+ try {
406
+ // Get session for this run
407
+ const session = this.sessionManager.getSession(message.sessionId);
408
+ if (!session) {
409
+ this.sendError('SESSION_NOT_FOUND', 'Session not found', {
410
+ runId,
411
+ graphId: message.graphId
412
+ });
413
+ return;
414
+ }
415
+
416
+ if (!message.graph) {
417
+ this.sendError('INVALID_GRAPH', 'Graph not provided', {
418
+ runId,
419
+ graphId: message.graphId
420
+ });
421
+ return;
422
+ }
423
+
424
+ // Create transport logger that forwards log messages to the client
425
+ const transportLogger = this.createTransportLogger(
426
+ runId,
427
+ message.graphId
428
+ );
429
+
430
+ // Ensure lifecycle event emitter and logger are available in registry
431
+ let registryToUse = this.registry;
432
+
433
+ // Apply session registry overrides if provided
434
+ if (session.config.registryOverrides) {
435
+ registryToUse = {
436
+ ...registryToUse,
437
+ ...session.config.registryOverrides
438
+ };
439
+ }
440
+
441
+ if (
442
+ !registryToUse.dependencies?.ILifecycleEventEmitter ||
443
+ !registryToUse.dependencies?.ILogger
444
+ ) {
445
+ // Create a new registry with required dependencies injected
446
+ registryToUse = {
447
+ ...registryToUse,
448
+ dependencies: {
449
+ ...registryToUse.dependencies,
450
+ ILifecycleEventEmitter:
451
+ registryToUse.dependencies?.ILifecycleEventEmitter ||
452
+ new ManualLifecycleEventEmitter(),
453
+ ILogger: transportLogger
454
+ }
455
+ };
456
+ } else {
457
+ // Replace the existing logger with the transport logger
458
+ registryToUse = {
459
+ ...registryToUse,
460
+ dependencies: {
461
+ ...registryToUse.dependencies,
462
+ ILogger: transportLogger
463
+ }
464
+ };
465
+ }
466
+
467
+ // Parse graph with registry that has lifecycle event emitter
468
+ const graphInstance = readGraphFromJSON({
469
+ graphJson: message.graph,
470
+ registry: registryToUse
471
+ });
472
+
473
+ // Validate graph
474
+ const errors = validateGraph(graphInstance);
475
+ if (errors.length > 0) {
476
+ this.sendError('VALIDATION_FAILED', errors.join('; '), {
477
+ runId,
478
+ graphId: message.graphId
479
+ });
480
+ return;
481
+ }
482
+
483
+ // Create engine - it will now have access to lifecycle event emitter through graph instance
484
+ const engine = new Engine(graphInstance, registryToUse);
485
+
486
+ // Merge execution options: message options override session defaults
487
+ const executionOptions = {
488
+ ...session.config.defaultExecutionOptions,
489
+ ...message.options
490
+ };
491
+
492
+ // Create run record
493
+ const run: ActiveRun = {
494
+ runId,
495
+ sessionId: message.sessionId,
496
+ graphId: message.graphId,
497
+
498
+ engine,
499
+ graphInstance,
500
+ registry: registryToUse,
501
+ status: 'running',
502
+ startedAt: Date.now(),
503
+ performance: {
504
+ nodesExecuted: 0,
505
+ eventsEmitted: 0,
506
+ variableChanges: 0
507
+ },
508
+ isPaused: false,
509
+ executionPhase: 'start',
510
+ currentTick: 0,
511
+ maxTicks: Infinity // No limit - tick events run until stopped
512
+ };
513
+
514
+ this.activeRuns.set(runId, run);
515
+ this.sessionManager.addRunToSession(message.sessionId, runId);
516
+
517
+ // Call session hook for run started
518
+ if (session.config.hooks?.onRunStarted) {
519
+ await session.config.hooks.onRunStarted(
520
+ session,
521
+ runId,
522
+ message.graphId
523
+ );
524
+ }
525
+
526
+ // Send run started
527
+ this.notifyMessage({
528
+ type: 'runStarted',
529
+ runId,
530
+ graphId: message.graphId,
531
+ startedAt: run.startedAt
532
+ });
533
+
534
+ // Update store state
535
+ this.updateStoreActiveRuns();
536
+ this.updateStoreExecutionState(true, false);
537
+
538
+ // Set up variable change tracking
539
+ setupVariableChangeTracking(run, message.graphId, {
540
+ sendMessage: this.notifyMessage.bind(this),
541
+ sendError: (code, msg, details) => this.sendError(code, msg, details)
542
+ });
543
+
544
+ // Set up tracing
545
+ if (executionOptions.trace) {
546
+ engine.onNodeExecutionStart.addListener((node) => {
547
+ run.performance.nodesExecuted++;
548
+ this.notifyMessage({
549
+ type: 'trace',
550
+ runId,
551
+ graphId: message.graphId,
552
+ nodeId: node.id,
553
+ event: 'start',
554
+ data: { typeName: node.description.typeName },
555
+ timestamp: Date.now() - run.startedAt
556
+ });
557
+ });
558
+
559
+ engine.onNodeExecutionEnd.addListener((node) => {
560
+ this.notifyMessage({
561
+ type: 'trace',
562
+ runId,
563
+ graphId: message.graphId,
564
+ nodeId: node.id,
565
+ event: 'end',
566
+ data: { typeName: node.description.typeName },
567
+ timestamp: Date.now() - run.startedAt
568
+ });
569
+ });
570
+ }
571
+
572
+ // Execute graph asynchronously
573
+ const autoEnd = executionOptions.autoEnd ?? true;
574
+ this.executeGraph(run, message.graphId, autoEnd, session);
575
+ } catch (error) {
576
+ const errorMessage =
577
+ error instanceof Error ? error.message : String(error);
578
+
579
+ // Get session for error hook
580
+ const session = this.sessionManager.getSession(message.sessionId);
581
+ if (session?.config.hooks?.onRunError) {
582
+ await session.config.hooks.onRunError(
583
+ session,
584
+ runId,
585
+ message.graphId,
586
+ error instanceof Error ? error : new Error(errorMessage)
587
+ );
588
+ }
589
+
590
+ this.sendError('NODE_EXECUTION_ERROR', errorMessage, {
591
+ runId,
592
+ graphId: message.graphId
593
+ });
594
+ }
595
+ }
596
+
597
+ private async executeGraph(
598
+ run: ActiveRun,
599
+ graphId: string,
600
+ autoEnd: boolean,
601
+ session: Session
602
+ ): Promise<void> {
603
+ try {
604
+ // Get lifecycle event emitter from the registry dependencies
605
+ const eventEmitter = run.registry.dependencies?.ILifecycleEventEmitter as
606
+ | ILifecycleEventEmitter
607
+ | undefined;
608
+
609
+ // Execute start event
610
+ if (run.executionPhase === 'start') {
611
+ if (
612
+ eventEmitter?.startEvent &&
613
+ eventEmitter.startEvent.listenerCount > 0
614
+ ) {
615
+ eventEmitter.startEvent.emit();
616
+ await this.executeWithPauseSupport(run);
617
+ }
618
+ run.executionPhase = 'tick';
619
+ }
620
+
621
+ // Execute tick events (runs indefinitely until stopped)
622
+ if (run.executionPhase === 'tick') {
623
+ if (
624
+ eventEmitter?.tickEvent &&
625
+ eventEmitter.tickEvent.listenerCount > 0
626
+ ) {
627
+ // Get tick strategy hook or create default
628
+ const tickStrategy =
629
+ session.config.tickStrategy ||
630
+ this.createSleepTickStrategy(
631
+ session.config.executionSettings?.tickInterval ??
632
+ this.getTickInterval()
633
+ );
634
+
635
+ while (!run.isPaused && run.status === 'running') {
636
+ eventEmitter.tickEvent.emit();
637
+ await this.executeWithPauseSupport(run);
638
+ run.currentTick++;
639
+
640
+ if (run.isPaused || run.status !== 'running') {
641
+ return; // Exit early if paused or stopped
642
+ }
643
+
644
+ // Call the tick strategy hook to handle timing
645
+ await tickStrategy();
646
+ }
647
+ } else {
648
+ // No tick event listeners, move to end phase
649
+ run.executionPhase = 'end';
650
+ }
651
+ }
652
+
653
+ // Execute end event
654
+ if (run.executionPhase === 'end' && !run.isPaused) {
655
+ if (eventEmitter?.endEvent && eventEmitter.endEvent.listenerCount > 0) {
656
+ eventEmitter.endEvent.emit();
657
+ await this.executeWithPauseSupport(run);
658
+ }
659
+ run.executionPhase = 'completed';
660
+ }
661
+
662
+ // Only complete if not paused
663
+ if (!run.isPaused && !autoEnd) {
664
+ // Run completed successfully
665
+ run.status = 'completed';
666
+ const elapsedMs = Date.now() - run.startedAt;
667
+ const result = null; // Placeholder for result
668
+
669
+ // Call session hook for run completed
670
+ if (session.config.hooks?.onRunCompleted) {
671
+ await session.config.hooks.onRunCompleted(
672
+ session,
673
+ run.runId,
674
+ graphId,
675
+ result
676
+ );
677
+ }
678
+
679
+ this.notifyMessage({
680
+ type: 'completed',
681
+ runId: run.runId,
682
+ graphId,
683
+ completedAt: Date.now(),
684
+ elapsedMs,
685
+ result,
686
+ performance: run.performance
687
+ });
688
+
689
+ // Cleanup
690
+ run.engine.dispose();
691
+ this.activeRuns.delete(run.runId);
692
+ this.sessionManager.removeRunFromSession(run.sessionId, run.runId);
693
+ this.updateStoreActiveRuns();
694
+ this.updateStoreExecutionState(false, false);
695
+ }
696
+ } catch (error) {
697
+ run.status = 'error';
698
+ const errorMessage =
699
+ error instanceof Error ? error.message : String(error);
700
+
701
+ // Call session hook for run error
702
+ if (session.config.hooks?.onRunError) {
703
+ await session.config.hooks.onRunError(
704
+ session,
705
+ run.runId,
706
+ graphId,
707
+ error instanceof Error ? error : new Error(errorMessage)
708
+ );
709
+ }
710
+
711
+ this.sendError('NODE_EXECUTION_ERROR', errorMessage, {
712
+ runId: run.runId,
713
+ graphId
714
+ });
715
+
716
+ run.engine.dispose();
717
+ this.activeRuns.delete(run.runId);
718
+ this.sessionManager.removeRunFromSession(run.sessionId, run.runId);
719
+ this.updateStoreActiveRuns();
720
+ this.updateStoreExecutionState(false, false);
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Execute engine with pause support - executes one step at a time with configurable delay
726
+ */
727
+ private async executeWithPauseSupport(run: ActiveRun): Promise<void> {
728
+ const session = this.sessionManager.getSession(run.sessionId);
729
+
730
+ // Get settings from session, fallback to store
731
+ const stepDelay =
732
+ session?.config.executionSettings?.stepDelay ??
733
+ this.store?.getState().stepDelay ??
734
+ 0;
735
+ const executionSpeed =
736
+ session?.config.executionSettings?.executionSpeed ??
737
+ this.store?.getState().executionSpeed ??
738
+ 1.0;
739
+
740
+ const stepLimit = this.getExecutionStepLimit();
741
+ const delay =
742
+ stepDelay + (executionSpeed < 1.0 ? (1.0 - executionSpeed) * 100 : 0);
743
+
744
+ // Loop while engine has pending work and not paused
745
+ while (run.engine.hasPending() && !run.isPaused) {
746
+ // Execute limited number of steps
747
+ await run.engine.executeAllAsync(5, stepLimit);
748
+
749
+ // Apply delay between successive calls
750
+ if (delay > 0 && run.engine.hasPending() && !run.isPaused) {
751
+ await sleep(delay / 1000);
752
+ }
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Pause execution of a running graph
758
+ */
759
+ public pauseExecution(runId: string): void {
760
+ const run = this.activeRuns.get(runId);
761
+ if (!run) {
762
+ this.updateStoreExecutionState(true, true);
763
+ throw new Error(`Run not found: ${runId}`);
764
+ }
765
+ run.isPaused = true;
766
+ run.status = 'running'; // Keep as running but paused
767
+ }
768
+
769
+ /**
770
+ * Resume execution of a paused graph
771
+ */
772
+ public async resumeExecution(runId: string): Promise<void> {
773
+ this.updateStoreExecutionState(true, false);
774
+ const run = this.activeRuns.get(runId);
775
+ if (!run) {
776
+ throw new Error(`Run not found: ${runId}`);
777
+ }
778
+
779
+ const session = this.sessionManager.getSession(run.sessionId);
780
+ if (!session) {
781
+ throw new Error(`Session not found: ${run.sessionId}`);
782
+ }
783
+
784
+ run.isPaused = false;
785
+ // Continue execution from where we left off
786
+ await this.executeGraph(run, run.graphId, true, session);
787
+ }
788
+
789
+ /**
790
+ * Step forward one execution step
791
+ */
792
+ public async stepExecution(runId: string): Promise<void> {
793
+ const run = this.activeRuns.get(runId);
794
+ if (!run) {
795
+ throw new Error(`Run not found: ${runId}`);
796
+ }
797
+
798
+ const eventEmitter = run.registry.dependencies?.ILifecycleEventEmitter as
799
+ | ILifecycleEventEmitter
800
+ | undefined;
801
+
802
+ // Execute one step based on current phase
803
+ if (run.executionPhase === 'start') {
804
+ if (
805
+ eventEmitter?.startEvent &&
806
+ eventEmitter.startEvent.listenerCount > 0
807
+ ) {
808
+ eventEmitter.startEvent.emit();
809
+ }
810
+ // Execute one fiber step
811
+ await run.engine.executeAllSync(5, 1);
812
+
813
+ // Check if we should move to next phase
814
+ if (!run.engine.hasPending()) {
815
+ run.executionPhase = 'tick';
816
+ }
817
+ } else if (run.executionPhase === 'tick') {
818
+ if (run.currentTick < run.maxTicks) {
819
+ if (
820
+ eventEmitter?.tickEvent &&
821
+ eventEmitter.tickEvent.listenerCount > 0 &&
822
+ !run.engine.hasPending()
823
+ ) {
824
+ eventEmitter.tickEvent.emit();
825
+ }
826
+ // Execute one fiber step
827
+ await run.engine.executeAllSync(5, 1);
828
+
829
+ // Check if current tick is done
830
+ if (!run.engine.hasPending()) {
831
+ run.currentTick++;
832
+ if (run.currentTick >= run.maxTicks) {
833
+ run.executionPhase = 'end';
834
+ }
835
+ }
836
+ } else {
837
+ run.executionPhase = 'end';
838
+ }
839
+ } else if (run.executionPhase === 'end') {
840
+ if (
841
+ eventEmitter?.endEvent &&
842
+ eventEmitter.endEvent.listenerCount > 0 &&
843
+ !run.engine.hasPending()
844
+ ) {
845
+ eventEmitter.endEvent.emit();
846
+ }
847
+ // Execute one fiber step
848
+ await run.engine.executeAllSync(5, 1);
849
+
850
+ // Check if we're done
851
+ if (!run.engine.hasPending()) {
852
+ run.executionPhase = 'completed';
853
+
854
+ // Run completed successfully
855
+ run.status = 'completed';
856
+ const elapsedMs = Date.now() - run.startedAt;
857
+ const result = null;
858
+
859
+ // Call session hook for run completed
860
+ const session = this.sessionManager.getSession(run.sessionId);
861
+ if (session?.config.hooks?.onRunCompleted) {
862
+ await session.config.hooks.onRunCompleted(
863
+ session,
864
+ run.runId,
865
+ run.graphId,
866
+ result
867
+ );
868
+ }
869
+
870
+ this.notifyMessage({
871
+ type: 'completed',
872
+ runId: run.runId,
873
+ graphId: run.graphId,
874
+ completedAt: Date.now(),
875
+ elapsedMs,
876
+ result,
877
+ performance: run.performance
878
+ });
879
+
880
+ // Cleanup
881
+ run.engine.dispose();
882
+ this.activeRuns.delete(run.runId);
883
+ this.sessionManager.removeRunFromSession(run.sessionId, run.runId);
884
+ }
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Check if a run is currently paused
890
+ */
891
+ public isPaused(runId: string): boolean {
892
+ const run = this.activeRuns.get(runId);
893
+ return run?.isPaused ?? false;
894
+ }
895
+
896
+ private handleStopGraph(message: StopGraphMessage): void {
897
+ const run = this.activeRuns.get(message.runId);
898
+ if (!run) {
899
+ this.sendError('RUN_NOT_FOUND', 'Run not found', {
900
+ runId: message.runId
901
+ });
902
+ return;
903
+ }
904
+
905
+ run.status = 'stopped';
906
+ this.updateStoreActiveRuns();
907
+ this.updateStoreExecutionState(false, false);
908
+ run.engine.dispose();
909
+ this.activeRuns.delete(message.runId);
910
+ this.sessionManager.removeRunFromSession(run.sessionId, message.runId);
911
+
912
+ this.notifyMessage({
913
+ type: 'stopped',
914
+ runId: message.runId,
915
+ graphId: run.graphId,
916
+ reason: 'User requested stop'
917
+ });
918
+ }
919
+
920
+ private handleGetStatus(message: GetStatusMessage): void {
921
+ const run = this.activeRuns.get(message.runId);
922
+ if (!run) {
923
+ this.sendError('RUN_NOT_FOUND', 'Run not found', {
924
+ runId: message.runId
925
+ });
926
+ return;
927
+ }
928
+
929
+ const elapsedMs = Date.now() - run.startedAt;
930
+
931
+ this.notifyMessage({
932
+ type: 'status',
933
+ runId: run.runId,
934
+ graphId: run.graphId,
935
+ status: run.status,
936
+ startedAt: run.startedAt,
937
+ elapsedMs,
938
+ performance: run.performance,
939
+ startedGraphs: []
940
+ });
941
+ }
942
+
943
+ private async handleCloseSession(
944
+ message: CloseSessionMessage
945
+ ): Promise<void> {
946
+ const session = this.sessionManager.getSession(message.sessionId);
947
+
948
+ if (session) {
949
+ // Clean up all runs in this session
950
+ for (const run of this.activeRuns.values()) {
951
+ if (run.sessionId === message.sessionId) {
952
+ run.engine.dispose();
953
+ this.activeRuns.delete(run.runId);
954
+ }
955
+ }
956
+
957
+ this.updateStoreActiveRuns();
958
+ this.updateStoreExecutionState(false, false);
959
+
960
+ // Close the session (calls session hooks)
961
+ await this.sessionManager.closeSession(message.sessionId);
962
+ }
963
+
964
+ this.notifyMessage({
965
+ type: 'sessionClosed',
966
+ sessionId: message.sessionId
967
+ });
968
+ }
969
+
970
+ private sendError(
971
+ code: string,
972
+ message: string,
973
+ details?: Record<string, unknown>
974
+ ): void {
975
+ this.notifyMessage({
976
+ type: 'error',
977
+ code: code as any,
978
+ message,
979
+ ...details
980
+ });
981
+ }
982
+
983
+ // Realtime modification handlers
984
+
985
+ private handleAddNode(message: AddNodeMessage): void {
986
+ const run = this.activeRuns.get(message.runId);
987
+ if (!run) {
988
+ this.sendError('RUN_NOT_FOUND', `Run ${message.runId} not found`);
989
+ return;
990
+ }
991
+
992
+ try {
993
+ run.graphInstance.nodes[message.nodeId] = createNode({
994
+ id: message.nodeId,
995
+ nodeTypeName: message.nodeType,
996
+ nodeConfiguration: message.nodeData?.configuration || {},
997
+ registry: run.registry,
998
+ graph: makeGraphApi({
999
+ ...run.registry,
1000
+ variables: run.graphInstance.variables,
1001
+ customEvents: run.graphInstance.customEvents
1002
+ })
1003
+ });
1004
+
1005
+ this.notifyMessage({
1006
+ type: 'nodeAdded',
1007
+ runId: message.runId,
1008
+ graphId: run.graphId,
1009
+ nodeId: message.nodeId,
1010
+ nodeType: message.nodeType,
1011
+ nodeData: message.nodeData
1012
+ });
1013
+ } catch (error) {
1014
+ this.sendError(
1015
+ 'NODE_EXECUTION_ERROR',
1016
+ `Failed to add node: ${error instanceof Error ? error.message : String(error)}`
1017
+ );
1018
+ }
1019
+ }
1020
+
1021
+ private handleRemoveNode(message: RemoveNodeMessage): void {
1022
+ const run = this.activeRuns.get(message.runId);
1023
+ if (!run) {
1024
+ this.sendError('RUN_NOT_FOUND', `Run ${message.runId} not found`);
1025
+ return;
1026
+ }
1027
+
1028
+ const node = run.graphInstance.nodes?.[message.nodeId];
1029
+ if (!node) {
1030
+ this.sendError(
1031
+ 'NODE_EXECUTION_ERROR',
1032
+ `Node ${message.nodeId} not found in graph`
1033
+ );
1034
+ return;
1035
+ }
1036
+
1037
+ try {
1038
+ // Clean up links connected to this node's sockets
1039
+ for (const socket of [...(node.inputs || []), ...(node.outputs || [])]) {
1040
+ // Clear all links by removing them one by one
1041
+ while (socket.links.length > 0) {
1042
+ socket.links.pop();
1043
+ }
1044
+ }
1045
+
1046
+ // Also clean up links pointing TO this node from other nodes
1047
+ for (const otherNode of Object.values(run.graphInstance.nodes || {})) {
1048
+ if (otherNode && otherNode !== node) {
1049
+ for (const inputSocket of otherNode.inputs || []) {
1050
+ for (let i = inputSocket.links.length - 1; i >= 0; i--) {
1051
+ const link = inputSocket.links[i];
1052
+ if (link && link.nodeId === message.nodeId) {
1053
+ inputSocket.links.splice(i, 1);
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ // Remove the node from the graph
1061
+ delete run.graphInstance.nodes[message.nodeId];
1062
+
1063
+ // Notify client
1064
+ this.notifyMessage({
1065
+ type: 'nodeRemoved',
1066
+ runId: message.runId,
1067
+ graphId: run.graphId,
1068
+ nodeId: message.nodeId
1069
+ });
1070
+ } catch (error) {
1071
+ this.sendError(
1072
+ 'NODE_EXECUTION_ERROR',
1073
+ `Failed to remove node: ${error instanceof Error ? error.message : String(error)}`
1074
+ );
1075
+ }
1076
+ }
1077
+
1078
+ private handleUpdateSocketValue(message: UpdateSocketValueMessage): void {
1079
+ const run = this.activeRuns.get(message.runId);
1080
+ if (!run) {
1081
+ this.sendError('RUN_NOT_FOUND', `Run ${message.runId} not found`);
1082
+ return;
1083
+ }
1084
+
1085
+ const node = run.graphInstance.nodes?.[message.nodeId];
1086
+ if (!node) {
1087
+ this.sendError(
1088
+ 'NODE_EXECUTION_ERROR',
1089
+ `Node ${message.nodeId} not found in graph`
1090
+ );
1091
+ return;
1092
+ }
1093
+
1094
+ try {
1095
+ // Find the socket by name
1096
+ const socket = node.inputs.find((s) => s.name === message.socketName);
1097
+ if (!socket) {
1098
+ this.sendError(
1099
+ 'NODE_EXECUTION_ERROR',
1100
+ `Socket ${message.socketName} not found on node`
1101
+ );
1102
+ return;
1103
+ }
1104
+
1105
+ // Update the socket value
1106
+ socket.value = message.value;
1107
+
1108
+ // Notify about the change
1109
+ this.notifyMessage({
1110
+ type: 'trace',
1111
+ runId: message.runId,
1112
+ graphId: run.graphId,
1113
+ nodeId: message.nodeId,
1114
+ event: 'socketUpdated',
1115
+ data: { socketName: message.socketName, value: message.value },
1116
+ timestamp: Date.now() - run.startedAt
1117
+ });
1118
+ } catch (error) {
1119
+ this.sendError(
1120
+ 'NODE_EXECUTION_ERROR',
1121
+ `Failed to update socket: ${error instanceof Error ? error.message : String(error)}`
1122
+ );
1123
+ }
1124
+ }
1125
+
1126
+ private handleUpdateNodeParam(message: UpdateNodeParamMessage): void {
1127
+ const run = this.activeRuns.get(message.runId);
1128
+ if (!run) {
1129
+ this.sendError('RUN_NOT_FOUND', `Run ${message.runId} not found`);
1130
+ return;
1131
+ }
1132
+
1133
+ const node = run.graphInstance.nodes?.[message.nodeId];
1134
+ if (!node) {
1135
+ this.sendError(
1136
+ 'NODE_EXECUTION_ERROR',
1137
+ `Node ${message.nodeId} not found in graph`
1138
+ );
1139
+ return;
1140
+ }
1141
+
1142
+ try {
1143
+ // Store old value for delta
1144
+ const oldValue = node.configuration[message.paramName];
1145
+
1146
+ // Update the parameter
1147
+ node.configuration[message.paramName] = message.value;
1148
+
1149
+ // Notify about the change
1150
+ this.notifyMessage({
1151
+ type: 'nodeParamUpdated',
1152
+ runId: message.runId,
1153
+ graphId: run.graphId,
1154
+ nodeId: message.nodeId,
1155
+ paramName: message.paramName,
1156
+ oldValue,
1157
+ newValue: message.value
1158
+ });
1159
+ } catch (error) {
1160
+ this.sendError(
1161
+ 'NODE_EXECUTION_ERROR',
1162
+ `Failed to update parameter: ${error instanceof Error ? error.message : String(error)}`
1163
+ );
1164
+ }
1165
+ }
1166
+
1167
+ private handleCreateLink(message: CreateLinkMessage): void {
1168
+ const run = this.activeRuns.get(message.runId);
1169
+ if (!run) {
1170
+ this.sendError('RUN_NOT_FOUND', `Run ${message.runId} not found`);
1171
+ return;
1172
+ }
1173
+
1174
+ try {
1175
+ const fromNode = run.graphInstance.nodes?.[message.fromNodeId];
1176
+ const toNode = run.graphInstance.nodes?.[message.toNodeId];
1177
+
1178
+ if (!fromNode) {
1179
+ this.sendError('NODE_EXECUTION_ERROR', `Src node not found in graph`);
1180
+ return;
1181
+ }
1182
+ if (!toNode) {
1183
+ this.sendError('NODE_EXECUTION_ERROR', `Dest node not found in graph`);
1184
+ return;
1185
+ }
1186
+
1187
+ // Find the output socket on the source node
1188
+ const fromSocket = fromNode.outputs.find(
1189
+ (s) => s.name === message.fromSocket
1190
+ );
1191
+ if (!fromSocket) {
1192
+ this.sendError(
1193
+ 'NODE_EXECUTION_ERROR',
1194
+ `Output socket ${message.fromSocket} not found on source node`
1195
+ );
1196
+ return;
1197
+ }
1198
+
1199
+ // Find the input socket on the target node
1200
+ const toSocket = toNode.inputs.find((s) => s.name === message.toSocket);
1201
+ if (!toSocket) {
1202
+ this.sendError(
1203
+ 'NODE_EXECUTION_ERROR',
1204
+ `Input socket ${message.toSocket} not found on target node`
1205
+ );
1206
+ return;
1207
+ }
1208
+
1209
+ // Create the link
1210
+ const link = new Link(message.fromNodeId, message.fromSocket);
1211
+ toSocket.links.push(link);
1212
+
1213
+ // Notify about the change
1214
+ this.notifyMessage({
1215
+ type: 'linkCreated',
1216
+ runId: message.runId,
1217
+ graphId: run.graphId,
1218
+ fromNodeId: message.fromNodeId,
1219
+ fromSocket: message.fromSocket,
1220
+ toNodeId: message.toNodeId,
1221
+ toSocket: message.toSocket
1222
+ });
1223
+ } catch (error) {
1224
+ this.sendError(
1225
+ 'NODE_EXECUTION_ERROR',
1226
+ `Failed to create link: ${error instanceof Error ? error.message : String(error)}`
1227
+ );
1228
+ }
1229
+ }
1230
+
1231
+ private handleRemoveLink(message: RemoveLinkMessage): void {
1232
+ const run = this.activeRuns.get(message.runId);
1233
+ if (!run) {
1234
+ this.sendError('RUN_NOT_FOUND', `Run ${message.runId} not found`);
1235
+ return;
1236
+ }
1237
+
1238
+ try {
1239
+ const toNode = run.graphInstance.nodes?.[message.toNodeId];
1240
+ if (!toNode) {
1241
+ this.sendError(
1242
+ 'NODE_EXECUTION_ERROR',
1243
+ `Target node ${message.toNodeId} not found in graph`
1244
+ );
1245
+ return;
1246
+ }
1247
+
1248
+ // Find the input socket on the target node
1249
+ const toSocket = toNode.inputs.find((s) => s.name === message.toSocket);
1250
+ if (!toSocket) {
1251
+ this.sendError(
1252
+ 'NODE_EXECUTION_ERROR',
1253
+ `Input socket ${message.toSocket} not found on target node`
1254
+ );
1255
+ return;
1256
+ }
1257
+
1258
+ // Remove the matching link
1259
+ for (let i = toSocket.links.length - 1; i >= 0; i--) {
1260
+ const link = toSocket.links[i];
1261
+ if (
1262
+ link &&
1263
+ link.nodeId === message.fromNodeId &&
1264
+ link.socketName === message.fromSocket
1265
+ ) {
1266
+ toSocket.links.splice(i, 1);
1267
+ }
1268
+ }
1269
+
1270
+ // Notify about the change
1271
+ this.notifyMessage({
1272
+ type: 'linkRemoved',
1273
+ runId: message.runId,
1274
+ graphId: run.graphId,
1275
+ fromNodeId: message.fromNodeId,
1276
+ fromSocket: message.fromSocket,
1277
+ toNodeId: message.toNodeId,
1278
+ toSocket: message.toSocket
1279
+ });
1280
+ } catch (error) {
1281
+ this.sendError(
1282
+ 'NODE_EXECUTION_ERROR',
1283
+ `Failed to remove link: ${error instanceof Error ? error.message : String(error)}`
1284
+ );
1285
+ }
1286
+ }
1287
+
1288
+ private handleDirectExecuteNode(message: DirectExecuteNodeMessage): void {
1289
+ const run = this.activeRuns.get(message.runId);
1290
+ if (!run) {
1291
+ this.sendError('RUN_NOT_FOUND', `Run ${message.runId} not found`);
1292
+ return;
1293
+ }
1294
+
1295
+ try {
1296
+ const node = run.graphInstance.nodes?.[message.nodeId];
1297
+ if (!node) {
1298
+ this.sendError(
1299
+ 'NODE_EXECUTION_ERROR',
1300
+ `Node ${message.nodeId} not found in graph`
1301
+ );
1302
+ return;
1303
+ }
1304
+
1305
+ // Find and update the input socket
1306
+ const inputSocket = node.inputs.find(
1307
+ (s) => s.name === message.inputSocketName
1308
+ );
1309
+ if (!inputSocket) {
1310
+ this.sendError(
1311
+ 'NODE_EXECUTION_ERROR',
1312
+ `Input socket ${message.inputSocketName} not found on node`
1313
+ );
1314
+ return;
1315
+ }
1316
+
1317
+ inputSocket.value = message.inputValue;
1318
+
1319
+ // Get downstream nodes
1320
+ const downstreamNodes = this.getDownstreamNodes(
1321
+ message.nodeId,
1322
+ run.graphInstance
1323
+ );
1324
+ const nodesToExecute = [message.nodeId, ...downstreamNodes];
1325
+
1326
+ // Execute the node and downstream
1327
+ this.notifyMessage({
1328
+ type: 'affectedNodes',
1329
+ runId: message.runId,
1330
+ graphId: run.graphId,
1331
+ nodeIds: nodesToExecute,
1332
+ reason: 'direct-execution'
1333
+ });
1334
+ } catch (error) {
1335
+ this.sendError(
1336
+ 'NODE_EXECUTION_ERROR',
1337
+ `Failed to execute node: ${error instanceof Error ? error.message : String(error)}`
1338
+ );
1339
+ }
1340
+ }
1341
+
1342
+ private getDownstreamNodes(nodeId: string, graph: GraphInstance): string[] {
1343
+ const downstream = new Set<string>();
1344
+ const visited = new Set<string>();
1345
+ const queue = [nodeId];
1346
+
1347
+ while (queue.length > 0) {
1348
+ const currentId = queue.shift()!;
1349
+ if (visited.has(currentId)) {
1350
+ continue;
1351
+ }
1352
+ visited.add(currentId);
1353
+
1354
+ const currentNode = graph.nodes?.[currentId];
1355
+ if (!currentNode) {
1356
+ continue;
1357
+ }
1358
+
1359
+ // Find all nodes that depend on this one via output socket links
1360
+ for (const outputSocket of currentNode.outputs) {
1361
+ for (const link of outputSocket.links) {
1362
+ if (!visited.has(link.nodeId)) {
1363
+ downstream.add(link.nodeId);
1364
+ queue.push(link.nodeId);
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ return Array.from(downstream);
1371
+ }
1372
+ }