ultravisor 1.0.2 → 1.0.4
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.
- package/.claude/launch.json +11 -0
- package/.claude/ultravisor-dev-config.json +3 -0
- package/.ultravisor.json +426 -0
- package/docs/README.md +63 -0
- package/package.json +12 -8
- package/source/Ultravisor.cjs +22 -3
- package/source/cli/Ultravisor-CLIProgram.cjs +35 -23
- package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
- package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
- package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +5 -3
- package/source/services/Ultravisor-ExecutionEngine.cjs +1039 -0
- package/source/services/Ultravisor-ExecutionManifest.cjs +399 -0
- package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
- package/source/services/Ultravisor-Hypervisor.cjs +38 -83
- package/source/services/Ultravisor-StateManager.cjs +241 -0
- package/source/services/Ultravisor-TaskTypeRegistry.cjs +143 -0
- package/source/services/tasks/Ultravisor-TaskType-Base.cjs +105 -0
- package/source/services/tasks/control/Ultravisor-TaskType-IfConditional.cjs +148 -0
- package/source/services/tasks/control/Ultravisor-TaskType-LaunchOperation.cjs +187 -0
- package/source/services/tasks/control/Ultravisor-TaskType-SplitExecute.cjs +184 -0
- package/source/services/tasks/data/Ultravisor-TaskType-ReplaceString.cjs +82 -0
- package/source/services/tasks/data/Ultravisor-TaskType-SetValues.cjs +81 -0
- package/source/services/tasks/data/Ultravisor-TaskType-StringAppender.cjs +101 -0
- package/source/services/tasks/file-io/Ultravisor-TaskType-ReadFile.cjs +103 -0
- package/source/services/tasks/file-io/Ultravisor-TaskType-WriteFile.cjs +117 -0
- package/source/services/tasks/interaction/Ultravisor-TaskType-ErrorMessage.cjs +54 -0
- package/source/services/tasks/interaction/Ultravisor-TaskType-ValueInput.cjs +62 -0
- package/source/web_server/Ultravisor-API-Server.cjs +237 -124
- package/test/Ultravisor_browser_tests.js +2226 -0
- package/test/Ultravisor_tests.js +1143 -5830
- package/webinterface/css/ultravisor.css +23 -0
- package/webinterface/package.json +6 -3
- package/webinterface/source/Pict-Application-Ultravisor.js +93 -73
- package/webinterface/source/cards/FlowCard-CSVTransform.js +43 -0
- package/webinterface/source/cards/FlowCard-Command.js +86 -0
- package/webinterface/source/cards/FlowCard-ComprehensionIntersect.js +40 -0
- package/webinterface/source/cards/FlowCard-Conditional.js +87 -0
- package/webinterface/source/cards/FlowCard-CopyFile.js +55 -0
- package/webinterface/source/cards/FlowCard-End.js +29 -0
- package/webinterface/source/cards/FlowCard-GetJSON.js +55 -0
- package/webinterface/source/cards/FlowCard-GetText.js +54 -0
- package/webinterface/source/cards/FlowCard-Histogram.js +176 -0
- package/webinterface/source/cards/FlowCard-LaunchOperation.js +82 -0
- package/webinterface/source/cards/FlowCard-ListFiles.js +55 -0
- package/webinterface/source/cards/FlowCard-MeadowCount.js +44 -0
- package/webinterface/source/cards/FlowCard-MeadowCreate.js +44 -0
- package/webinterface/source/cards/FlowCard-MeadowDelete.js +45 -0
- package/webinterface/source/cards/FlowCard-MeadowRead.js +46 -0
- package/webinterface/source/cards/FlowCard-MeadowReads.js +46 -0
- package/webinterface/source/cards/FlowCard-MeadowUpdate.js +44 -0
- package/webinterface/source/cards/FlowCard-ParseCSV.js +85 -0
- package/webinterface/source/cards/FlowCard-ReadJSON.js +54 -0
- package/webinterface/source/cards/FlowCard-ReadText.js +54 -0
- package/webinterface/source/cards/FlowCard-RestRequest.js +59 -0
- package/webinterface/source/cards/FlowCard-SendJSON.js +57 -0
- package/webinterface/source/cards/FlowCard-Solver.js +77 -0
- package/webinterface/source/cards/FlowCard-Start.js +29 -0
- package/webinterface/source/cards/FlowCard-TemplateString.js +77 -0
- package/webinterface/source/cards/FlowCard-WriteJSON.js +54 -0
- package/webinterface/source/cards/FlowCard-WriteText.js +54 -0
- package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
- package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
- package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +8 -8
- package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +6 -6
- package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +436 -0
- package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +45 -43
- package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +34 -89
- package/webinterface/source/views/PictView-Ultravisor-OperationList.js +128 -13
- package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
- package/webinterface/source/views/PictView-Ultravisor-Schedule.js +18 -53
- package/webinterface/source/views/PictView-Ultravisor-TimingView.js +27 -14
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +2 -1
- package/.babelrc +0 -6
- package/.browserslistrc +0 -1
- package/.browserslistrc-BACKUP +0 -1
- package/.gulpfile-quackage-config.json +0 -7
- package/.gulpfile-quackage.js +0 -2
- package/debug/Harness.js +0 -5
- package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
- package/source/services/Ultravisor-Operation.cjs +0 -200
- package/source/services/Ultravisor-Task.cjs +0 -349
- package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
- package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
- package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
- package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
- package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
- package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
- package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
- package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
- package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
- package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
- package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
- package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
- package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
- package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
- package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
- package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
- package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
- package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
- package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
- package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
- package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
- package/webinterface/.babelrc +0 -6
- package/webinterface/.browserslistrc +0 -1
- package/webinterface/.browserslistrc-BACKUP +0 -1
- package/webinterface/.gulpfile-quackage-config.json +0 -7
- package/webinterface/.gulpfile-quackage.js +0 -2
- package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
- package/webinterface/source/views/PictView-Ultravisor-TaskList.js +0 -248
- /package/docs/{cover.md → _cover.md} +0 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
const libPictService = require('pict-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event-driven graph executor for Ultravisor operations.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the old sequential task runner. Processes an operation's directed
|
|
7
|
+
* graph by following event connections between task nodes. State connections
|
|
8
|
+
* are resolved just-in-time when a task is triggered.
|
|
9
|
+
*/
|
|
10
|
+
class UltravisorExecutionEngine extends libPictService
|
|
11
|
+
{
|
|
12
|
+
constructor(pPict, pOptions, pServiceHash)
|
|
13
|
+
{
|
|
14
|
+
super(pPict, pOptions, pServiceHash);
|
|
15
|
+
|
|
16
|
+
this.serviceType = 'UltravisorExecutionEngine';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Append a timestamped entry to the execution context log and the fable logger.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} pContext - The execution context.
|
|
23
|
+
* @param {string} pMessage - The log message.
|
|
24
|
+
* @param {string} [pLevel] - Log level: 'info' (default), 'warn', 'error', 'trace'.
|
|
25
|
+
*/
|
|
26
|
+
_log(pContext, pMessage, pLevel)
|
|
27
|
+
{
|
|
28
|
+
let tmpLevel = pLevel || 'info';
|
|
29
|
+
pContext.Log.push(`[${new Date().toISOString()}] ${pMessage}`);
|
|
30
|
+
this.log[tmpLevel](`ExecutionEngine [${pContext.OperationHash || '?'}]: ${pMessage}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute an operation by its definition.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} pOperationDefinition - The operation definition with Graph.
|
|
37
|
+
* @param {object} [pInitialState] - Optional initial state overrides:
|
|
38
|
+
* GlobalState {object} - seed values for global state
|
|
39
|
+
* OperationState {object} - seed values for operation state
|
|
40
|
+
* RunMode {string} - 'production' | 'standard' | 'debug'
|
|
41
|
+
* @param {function} fCallback - function(pError, pExecutionContext)
|
|
42
|
+
*/
|
|
43
|
+
executeOperation(pOperationDefinition, pInitialState, fCallback)
|
|
44
|
+
{
|
|
45
|
+
if (typeof(pInitialState) === 'function')
|
|
46
|
+
{
|
|
47
|
+
fCallback = pInitialState;
|
|
48
|
+
pInitialState = {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!pOperationDefinition || !pOperationDefinition.Graph)
|
|
52
|
+
{
|
|
53
|
+
return fCallback(new Error('ExecutionEngine: operation definition must have a Graph.'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let tmpInitialState = pInitialState || {};
|
|
57
|
+
|
|
58
|
+
// Get services
|
|
59
|
+
let tmpManifestService = this.fable.servicesMap['UltravisorExecutionManifest']
|
|
60
|
+
? Object.values(this.fable.servicesMap['UltravisorExecutionManifest'])[0]
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
if (!tmpManifestService)
|
|
64
|
+
{
|
|
65
|
+
return fCallback(new Error('ExecutionEngine: UltravisorExecutionManifest service not found.'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create execution context with staging folder
|
|
69
|
+
let tmpContext = tmpManifestService.createExecutionContext(
|
|
70
|
+
pOperationDefinition, tmpInitialState.RunMode);
|
|
71
|
+
|
|
72
|
+
// Seed initial state
|
|
73
|
+
if (tmpInitialState.GlobalState && typeof(tmpInitialState.GlobalState) === 'object')
|
|
74
|
+
{
|
|
75
|
+
Object.assign(tmpContext.GlobalState, tmpInitialState.GlobalState);
|
|
76
|
+
}
|
|
77
|
+
if (tmpInitialState.OperationState && typeof(tmpInitialState.OperationState) === 'object')
|
|
78
|
+
{
|
|
79
|
+
Object.assign(tmpContext.OperationState, tmpInitialState.OperationState);
|
|
80
|
+
}
|
|
81
|
+
if (pOperationDefinition.InitialGlobalState && typeof(pOperationDefinition.InitialGlobalState) === 'object')
|
|
82
|
+
{
|
|
83
|
+
// Operation-level defaults (overridden by runtime initial state)
|
|
84
|
+
let tmpKeys = Object.keys(pOperationDefinition.InitialGlobalState);
|
|
85
|
+
for (let i = 0; i < tmpKeys.length; i++)
|
|
86
|
+
{
|
|
87
|
+
if (!tmpContext.GlobalState.hasOwnProperty(tmpKeys[i]))
|
|
88
|
+
{
|
|
89
|
+
tmpContext.GlobalState[tmpKeys[i]] = pOperationDefinition.InitialGlobalState[tmpKeys[i]];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (pOperationDefinition.InitialOperationState && typeof(pOperationDefinition.InitialOperationState) === 'object')
|
|
94
|
+
{
|
|
95
|
+
let tmpKeys = Object.keys(pOperationDefinition.InitialOperationState);
|
|
96
|
+
for (let i = 0; i < tmpKeys.length; i++)
|
|
97
|
+
{
|
|
98
|
+
if (!tmpContext.OperationState.hasOwnProperty(tmpKeys[i]))
|
|
99
|
+
{
|
|
100
|
+
tmpContext.OperationState[tmpKeys[i]] = pOperationDefinition.InitialOperationState[tmpKeys[i]];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Store the graph for lookup
|
|
106
|
+
tmpContext._Graph = pOperationDefinition.Graph;
|
|
107
|
+
tmpContext._NodeMap = this._buildNodeMap(pOperationDefinition.Graph);
|
|
108
|
+
tmpContext._PortLabelMap = this._buildPortLabelMap(pOperationDefinition.Graph);
|
|
109
|
+
tmpContext._ConnectionMap = this._buildConnectionMap(
|
|
110
|
+
pOperationDefinition.Graph, tmpContext._NodeMap, tmpContext._PortLabelMap);
|
|
111
|
+
|
|
112
|
+
// Mark as running
|
|
113
|
+
tmpContext.Status = 'Running';
|
|
114
|
+
tmpContext.StartTime = new Date().toISOString();
|
|
115
|
+
this._log(tmpContext, `Operation [${pOperationDefinition.Hash}] started.`);
|
|
116
|
+
|
|
117
|
+
// Find Start node and enqueue its output events
|
|
118
|
+
let tmpStartNode = this._findStartNode(tmpContext);
|
|
119
|
+
|
|
120
|
+
if (!tmpStartNode)
|
|
121
|
+
{
|
|
122
|
+
tmpContext.Status = 'Error';
|
|
123
|
+
this._log(tmpContext, 'No Start node found in the graph.');
|
|
124
|
+
tmpManifestService.finalizeExecution(tmpContext);
|
|
125
|
+
return fCallback(new Error('No Start node found in the graph.'), tmpContext);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fire all outgoing event connections from the Start node.
|
|
129
|
+
// Start nodes have a single output, so we enqueue all downstream targets
|
|
130
|
+
// without filtering on port name (avoids label vs hash naming mismatches).
|
|
131
|
+
this._enqueueAllDownstreamEvents(tmpStartNode.Hash, tmpContext);
|
|
132
|
+
|
|
133
|
+
// Process the event queue
|
|
134
|
+
this._processEventQueue(tmpContext,
|
|
135
|
+
(pError) =>
|
|
136
|
+
{
|
|
137
|
+
if (pError)
|
|
138
|
+
{
|
|
139
|
+
this._log(tmpContext, `Execution error: ${pError.message}`, 'error');
|
|
140
|
+
tmpContext.Errors.push({
|
|
141
|
+
NodeHash: null,
|
|
142
|
+
Message: pError.message,
|
|
143
|
+
Timestamp: new Date().toISOString()
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
tmpManifestService.finalizeExecution(tmpContext);
|
|
148
|
+
return fCallback(null, tmpContext);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resume a paused operation after a value-input task receives input.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} pRunHash - The execution run hash.
|
|
156
|
+
* @param {string} pNodeHash - The waiting task node hash.
|
|
157
|
+
* @param {*} pValue - The provided value.
|
|
158
|
+
* @param {function} fCallback - function(pError, pExecutionContext)
|
|
159
|
+
*/
|
|
160
|
+
resumeOperation(pRunHash, pNodeHash, pValue, fCallback)
|
|
161
|
+
{
|
|
162
|
+
let tmpManifestService = this.fable.servicesMap['UltravisorExecutionManifest']
|
|
163
|
+
? Object.values(this.fable.servicesMap['UltravisorExecutionManifest'])[0]
|
|
164
|
+
: null;
|
|
165
|
+
|
|
166
|
+
if (!tmpManifestService)
|
|
167
|
+
{
|
|
168
|
+
return fCallback(new Error('ExecutionEngine: UltravisorExecutionManifest service not found.'));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let tmpContext = tmpManifestService.getRun(pRunHash);
|
|
172
|
+
|
|
173
|
+
if (!tmpContext)
|
|
174
|
+
{
|
|
175
|
+
return fCallback(new Error(`ExecutionEngine: run [${pRunHash}] not found.`));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (tmpContext.Status !== 'WaitingForInput')
|
|
179
|
+
{
|
|
180
|
+
return fCallback(new Error(`ExecutionEngine: run [${pRunHash}] is not waiting for input (status: ${tmpContext.Status}).`));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let tmpWaitingInfo = tmpContext.WaitingTasks[pNodeHash];
|
|
184
|
+
|
|
185
|
+
if (!tmpWaitingInfo)
|
|
186
|
+
{
|
|
187
|
+
return fCallback(new Error(`ExecutionEngine: node [${pNodeHash}] is not waiting for input.`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Get the StateManager to write the value
|
|
191
|
+
let tmpStateManager = this._getStateManager();
|
|
192
|
+
|
|
193
|
+
// Write the value to the specified output address
|
|
194
|
+
if (tmpWaitingInfo.OutputAddress)
|
|
195
|
+
{
|
|
196
|
+
tmpStateManager.setAddress(tmpWaitingInfo.OutputAddress, pValue, tmpContext, pNodeHash);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Also store in task outputs
|
|
200
|
+
if (!tmpContext.TaskOutputs[pNodeHash])
|
|
201
|
+
{
|
|
202
|
+
tmpContext.TaskOutputs[pNodeHash] = {};
|
|
203
|
+
}
|
|
204
|
+
tmpContext.TaskOutputs[pNodeHash].InputValue = pValue;
|
|
205
|
+
|
|
206
|
+
// Remove from waiting list
|
|
207
|
+
delete tmpContext.WaitingTasks[pNodeHash];
|
|
208
|
+
|
|
209
|
+
// Fire the completion event
|
|
210
|
+
tmpContext.Status = 'Running';
|
|
211
|
+
this._log(tmpContext, `Value input received for node [${pNodeHash}], resuming execution.`);
|
|
212
|
+
|
|
213
|
+
this._enqueueDownstreamEvents(pNodeHash, 'ValueInputComplete', tmpContext);
|
|
214
|
+
|
|
215
|
+
// Process the event queue
|
|
216
|
+
this._processEventQueue(tmpContext,
|
|
217
|
+
(pError) =>
|
|
218
|
+
{
|
|
219
|
+
if (pError)
|
|
220
|
+
{
|
|
221
|
+
this._log(tmpContext, `Execution error after resume: ${pError.message}`, 'error');
|
|
222
|
+
}
|
|
223
|
+
tmpManifestService.finalizeExecution(tmpContext);
|
|
224
|
+
return fCallback(null, tmpContext);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ====================================================================
|
|
229
|
+
// Internal: Event Queue Processing
|
|
230
|
+
// ====================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Process events from the queue until it is empty.
|
|
234
|
+
*
|
|
235
|
+
* @param {object} pContext - The execution context.
|
|
236
|
+
* @param {function} fCallback - Called when queue is empty or operation is paused.
|
|
237
|
+
*/
|
|
238
|
+
_processEventQueue(pContext, fCallback)
|
|
239
|
+
{
|
|
240
|
+
if (pContext.PendingEvents.length === 0)
|
|
241
|
+
{
|
|
242
|
+
// Check if we're waiting for input
|
|
243
|
+
if (Object.keys(pContext.WaitingTasks).length > 0)
|
|
244
|
+
{
|
|
245
|
+
pContext.Status = 'WaitingForInput';
|
|
246
|
+
this._log(pContext, 'Operation paused: waiting for user input.');
|
|
247
|
+
}
|
|
248
|
+
return fCallback(null);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Dequeue the next event
|
|
252
|
+
let tmpEvent = pContext.PendingEvents.shift();
|
|
253
|
+
|
|
254
|
+
this._executeTaskForEvent(tmpEvent.TargetNodeHash, tmpEvent.EventName, pContext,
|
|
255
|
+
(pError) =>
|
|
256
|
+
{
|
|
257
|
+
if (pError)
|
|
258
|
+
{
|
|
259
|
+
this._log(pContext, `Error processing event [${tmpEvent.EventName}] on node [${tmpEvent.TargetNodeHash}]: ${pError.message}`, 'error');
|
|
260
|
+
// Continue processing other events despite errors
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Recurse to process next event
|
|
264
|
+
this._processEventQueue(pContext, fCallback);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Execute a task node in response to an incoming event.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} pNodeHash - The target node hash.
|
|
272
|
+
* @param {string} pEventName - The event that triggered this execution.
|
|
273
|
+
* @param {object} pContext - The execution context.
|
|
274
|
+
* @param {function} fCallback - Called when task execution is complete.
|
|
275
|
+
*/
|
|
276
|
+
_executeTaskForEvent(pNodeHash, pEventName, pContext, fCallback)
|
|
277
|
+
{
|
|
278
|
+
let tmpNode = pContext._NodeMap[pNodeHash];
|
|
279
|
+
|
|
280
|
+
if (!tmpNode)
|
|
281
|
+
{
|
|
282
|
+
this._log(pContext, `Node [${pNodeHash}] not found in graph.`, 'error');
|
|
283
|
+
return fCallback(null);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Handle built-in End node
|
|
287
|
+
if (tmpNode.Type === 'end')
|
|
288
|
+
{
|
|
289
|
+
this._log(pContext, `Reached End node [${pNodeHash}].`);
|
|
290
|
+
return fCallback(null);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Handle built-in Start node (shouldn't be a target, but handle gracefully)
|
|
294
|
+
if (tmpNode.Type === 'start')
|
|
295
|
+
{
|
|
296
|
+
return fCallback(null);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Find the task type
|
|
300
|
+
let tmpRegistry = this._getTaskTypeRegistry();
|
|
301
|
+
|
|
302
|
+
if (!tmpRegistry)
|
|
303
|
+
{
|
|
304
|
+
return fCallback(new Error('TaskTypeRegistry service not found.'));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let tmpDefinitionHash = tmpNode.DefinitionHash || tmpNode.Type;
|
|
308
|
+
let tmpDefinition = tmpRegistry.getDefinition(tmpDefinitionHash);
|
|
309
|
+
|
|
310
|
+
if (!tmpDefinition)
|
|
311
|
+
{
|
|
312
|
+
this._log(pContext, `Unknown task type [${tmpDefinitionHash}] for node [${pNodeHash}].`, 'error');
|
|
313
|
+
return fCallback(null);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Resolve incoming state connections
|
|
317
|
+
let tmpResolvedSettings = this._resolveStateConnections(pNodeHash, tmpNode, pContext);
|
|
318
|
+
|
|
319
|
+
// Get the manifest service for recording
|
|
320
|
+
let tmpManifestService = this._getManifestService();
|
|
321
|
+
if (tmpManifestService)
|
|
322
|
+
{
|
|
323
|
+
tmpManifestService.recordTaskStart(pContext, pNodeHash, pEventName);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this._log(pContext, `Executing node [${pNodeHash}] (${tmpDefinition.Name}) triggered by [${pEventName}]`);
|
|
327
|
+
|
|
328
|
+
// Create task instance and execute
|
|
329
|
+
let tmpTaskInstance = tmpRegistry.instantiateTaskType(tmpDefinitionHash);
|
|
330
|
+
|
|
331
|
+
if (!tmpTaskInstance)
|
|
332
|
+
{
|
|
333
|
+
this._log(pContext, `Failed to instantiate task type [${tmpDefinitionHash}].`, 'error');
|
|
334
|
+
return fCallback(null);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Build the per-task execution context
|
|
338
|
+
let tmpTaskContext = {
|
|
339
|
+
GlobalState: pContext.GlobalState,
|
|
340
|
+
OperationState: pContext.OperationState,
|
|
341
|
+
TaskOutputs: pContext.TaskOutputs,
|
|
342
|
+
StagingPath: pContext.StagingPath,
|
|
343
|
+
OperationHash: pContext.OperationHash,
|
|
344
|
+
NodeHash: pNodeHash,
|
|
345
|
+
RunHash: pContext.Hash,
|
|
346
|
+
RunMode: pContext.RunMode,
|
|
347
|
+
StateManager: this._getStateManager(),
|
|
348
|
+
TriggeringEventName: pEventName
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Build the fFireIntermediateEvent function for re-entrant tasks
|
|
352
|
+
let fFireIntermediateEvent = (pIntermediateEventName, pIntermediateOutputs, fResumeCallback) =>
|
|
353
|
+
{
|
|
354
|
+
// Store the intermediate outputs
|
|
355
|
+
if (!pContext.TaskOutputs[pNodeHash])
|
|
356
|
+
{
|
|
357
|
+
pContext.TaskOutputs[pNodeHash] = {};
|
|
358
|
+
}
|
|
359
|
+
Object.assign(pContext.TaskOutputs[pNodeHash], pIntermediateOutputs);
|
|
360
|
+
|
|
361
|
+
// Find downstream nodes for this intermediate event
|
|
362
|
+
let tmpDownstreamEvents = this._getDownstreamEvents(pNodeHash, pIntermediateEventName, pContext);
|
|
363
|
+
|
|
364
|
+
if (tmpDownstreamEvents.length === 0)
|
|
365
|
+
{
|
|
366
|
+
// No downstream connections for this event
|
|
367
|
+
return fResumeCallback();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Process the downstream sub-graph synchronously
|
|
371
|
+
let tmpSubIndex = 0;
|
|
372
|
+
|
|
373
|
+
let fProcessNextDownstream = () =>
|
|
374
|
+
{
|
|
375
|
+
if (tmpSubIndex >= tmpDownstreamEvents.length)
|
|
376
|
+
{
|
|
377
|
+
return fResumeCallback();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let tmpDownstreamEvent = tmpDownstreamEvents[tmpSubIndex];
|
|
381
|
+
tmpSubIndex++;
|
|
382
|
+
|
|
383
|
+
this._executeTaskForEvent(tmpDownstreamEvent.TargetNodeHash, tmpDownstreamEvent.EventName, pContext,
|
|
384
|
+
(pError) =>
|
|
385
|
+
{
|
|
386
|
+
if (pError)
|
|
387
|
+
{
|
|
388
|
+
this._log(pContext, `Error in sub-graph: ${pError.message}`, 'error');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Also process any events that were enqueued during sub-graph execution
|
|
392
|
+
this._drainEventsForSubgraph(pContext, () =>
|
|
393
|
+
{
|
|
394
|
+
fProcessNextDownstream();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
fProcessNextDownstream();
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Execute the task
|
|
403
|
+
tmpTaskInstance.execute(tmpResolvedSettings, tmpTaskContext, (pError, pResult) =>
|
|
404
|
+
{
|
|
405
|
+
if (pError)
|
|
406
|
+
{
|
|
407
|
+
this._log(pContext, `Task [${pNodeHash}] error: ${pError.message}`, 'error');
|
|
408
|
+
if (tmpManifestService)
|
|
409
|
+
{
|
|
410
|
+
tmpManifestService.recordTaskError(pContext, pNodeHash, pError);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fire error event if the task has one
|
|
414
|
+
this._enqueueDownstreamEvents(pNodeHash, 'Error', pContext);
|
|
415
|
+
return fCallback(null);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!pResult)
|
|
419
|
+
{
|
|
420
|
+
this._log(pContext, `Task [${pNodeHash}] returned no result.`, 'warn');
|
|
421
|
+
return fCallback(null);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check for WaitingForInput (value-input task)
|
|
425
|
+
if (pResult.WaitingForInput)
|
|
426
|
+
{
|
|
427
|
+
pContext.WaitingTasks[pNodeHash] = {
|
|
428
|
+
PromptMessage: pResult.PromptMessage || '',
|
|
429
|
+
OutputAddress: pResult.OutputAddress || '',
|
|
430
|
+
Timestamp: new Date().toISOString()
|
|
431
|
+
};
|
|
432
|
+
this._log(pContext, `Task [${pNodeHash}] is waiting for user input.`);
|
|
433
|
+
if (tmpManifestService)
|
|
434
|
+
{
|
|
435
|
+
tmpManifestService.recordTaskComplete(pContext, pNodeHash, pResult);
|
|
436
|
+
}
|
|
437
|
+
return fCallback(null);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Store outputs in TaskOutputs
|
|
441
|
+
if (pResult.Outputs && typeof(pResult.Outputs) === 'object')
|
|
442
|
+
{
|
|
443
|
+
if (!pContext.TaskOutputs[pNodeHash])
|
|
444
|
+
{
|
|
445
|
+
pContext.TaskOutputs[pNodeHash] = {};
|
|
446
|
+
}
|
|
447
|
+
Object.assign(pContext.TaskOutputs[pNodeHash], pResult.Outputs);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Store any state writes from the result
|
|
451
|
+
if (pResult.StateWrites && typeof(pResult.StateWrites) === 'object')
|
|
452
|
+
{
|
|
453
|
+
let tmpStateManager = this._getStateManager();
|
|
454
|
+
let tmpWriteKeys = Object.keys(pResult.StateWrites);
|
|
455
|
+
for (let i = 0; i < tmpWriteKeys.length; i++)
|
|
456
|
+
{
|
|
457
|
+
tmpStateManager.setAddress(tmpWriteKeys[i], pResult.StateWrites[tmpWriteKeys[i]],
|
|
458
|
+
pContext, pNodeHash);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Determine if this result is an error event
|
|
463
|
+
let tmpIsErrorResult = false;
|
|
464
|
+
if (pResult.EventToFire && tmpDefinition && Array.isArray(tmpDefinition.EventOutputs))
|
|
465
|
+
{
|
|
466
|
+
for (let e = 0; e < tmpDefinition.EventOutputs.length; e++)
|
|
467
|
+
{
|
|
468
|
+
if (tmpDefinition.EventOutputs[e].Name === pResult.EventToFire
|
|
469
|
+
&& tmpDefinition.EventOutputs[e].IsError)
|
|
470
|
+
{
|
|
471
|
+
tmpIsErrorResult = true;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Log task messages
|
|
478
|
+
if (Array.isArray(pResult.Log))
|
|
479
|
+
{
|
|
480
|
+
let tmpLogLevel = tmpIsErrorResult ? 'error' : 'trace';
|
|
481
|
+
for (let i = 0; i < pResult.Log.length; i++)
|
|
482
|
+
{
|
|
483
|
+
this._log(pContext, ` [${pNodeHash}] ${pResult.Log[i]}`, tmpLogLevel);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Record completion
|
|
488
|
+
if (tmpManifestService)
|
|
489
|
+
{
|
|
490
|
+
tmpManifestService.recordTaskComplete(pContext, pNodeHash, pResult);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Fire the output event (enqueue downstream tasks)
|
|
494
|
+
if (pResult.EventToFire)
|
|
495
|
+
{
|
|
496
|
+
let tmpQueueLenBefore = pContext.PendingEvents.length;
|
|
497
|
+
this._enqueueDownstreamEvents(pNodeHash, pResult.EventToFire, pContext);
|
|
498
|
+
let tmpHandled = pContext.PendingEvents.length > tmpQueueLenBefore;
|
|
499
|
+
|
|
500
|
+
// Record an unhandled error on the context when no downstream
|
|
501
|
+
// error handler is connected.
|
|
502
|
+
if (tmpIsErrorResult && !tmpHandled)
|
|
503
|
+
{
|
|
504
|
+
let tmpErrorMessage = (Array.isArray(pResult.Log) && pResult.Log.length > 0)
|
|
505
|
+
? pResult.Log.join('; ')
|
|
506
|
+
: `Task [${pNodeHash}] fired error event.`;
|
|
507
|
+
pContext.Errors.push({
|
|
508
|
+
NodeHash: pNodeHash,
|
|
509
|
+
Message: tmpErrorMessage,
|
|
510
|
+
Timestamp: new Date().toISOString()
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else if (tmpIsErrorResult)
|
|
515
|
+
{
|
|
516
|
+
// Error result with no EventToFire — still record the error
|
|
517
|
+
let tmpErrorMessage = (Array.isArray(pResult.Log) && pResult.Log.length > 0)
|
|
518
|
+
? pResult.Log.join('; ')
|
|
519
|
+
: `Task [${pNodeHash}] fired error event.`;
|
|
520
|
+
pContext.Errors.push({
|
|
521
|
+
NodeHash: pNodeHash,
|
|
522
|
+
Message: tmpErrorMessage,
|
|
523
|
+
Timestamp: new Date().toISOString()
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return fCallback(null);
|
|
528
|
+
},
|
|
529
|
+
fFireIntermediateEvent);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ====================================================================
|
|
533
|
+
// Internal: State Connection Resolution
|
|
534
|
+
// ====================================================================
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Resolve all incoming state connections for a node, producing merged settings.
|
|
538
|
+
*
|
|
539
|
+
* @param {string} pNodeHash - The target node hash.
|
|
540
|
+
* @param {object} pNode - The node definition from the graph.
|
|
541
|
+
* @param {object} pContext - The execution context.
|
|
542
|
+
* @returns {object} The resolved settings object.
|
|
543
|
+
*/
|
|
544
|
+
_resolveStateConnections(pNodeHash, pNode, pContext)
|
|
545
|
+
{
|
|
546
|
+
// Start with a copy of the node's static settings
|
|
547
|
+
let tmpSettings = {};
|
|
548
|
+
|
|
549
|
+
if (pNode.Settings && typeof(pNode.Settings) === 'object')
|
|
550
|
+
{
|
|
551
|
+
tmpSettings = JSON.parse(JSON.stringify(pNode.Settings));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Find all incoming State connections targeting this node
|
|
555
|
+
let tmpStateConnections = pContext._ConnectionMap.stateTargets[pNodeHash] || [];
|
|
556
|
+
let tmpStateManager = this._getStateManager();
|
|
557
|
+
let tmpPortLabelMap = pContext._PortLabelMap;
|
|
558
|
+
|
|
559
|
+
for (let i = 0; i < tmpStateConnections.length; i++)
|
|
560
|
+
{
|
|
561
|
+
let tmpConn = tmpStateConnections[i];
|
|
562
|
+
|
|
563
|
+
// Get the source port name
|
|
564
|
+
let tmpSourcePortName = this._extractPortName(tmpConn.SourcePortHash, tmpPortLabelMap);
|
|
565
|
+
let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
|
|
566
|
+
|
|
567
|
+
// Read the source value from the source node's outputs
|
|
568
|
+
let tmpSourceNodeOutputs = pContext.TaskOutputs[tmpConn.SourceNodeHash] || {};
|
|
569
|
+
let tmpSourceValue = tmpSourceNodeOutputs[tmpSourcePortName];
|
|
570
|
+
|
|
571
|
+
// Apply template if defined
|
|
572
|
+
if (tmpConn.Data && tmpConn.Data.Template && typeof(tmpConn.Data.Template) === 'string')
|
|
573
|
+
{
|
|
574
|
+
let tmpTemplateContext = tmpStateManager.buildTemplateContext(pContext, tmpSourceValue);
|
|
575
|
+
tmpSourceValue = this._resolveTemplate(tmpConn.Data.Template, tmpTemplateContext);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Write the resolved value into settings
|
|
579
|
+
if (tmpTargetPortName && tmpSourceValue !== undefined)
|
|
580
|
+
{
|
|
581
|
+
tmpSettings[tmpTargetPortName] = tmpSourceValue;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Resolve any template expressions in settings values
|
|
586
|
+
// (e.g. "{~D:Record.Operation.InputFilePath~}" -> actual value from state)
|
|
587
|
+
let tmpTemplateContext = tmpStateManager.buildTemplateContext(pContext);
|
|
588
|
+
|
|
589
|
+
let tmpSettingsKeys = Object.keys(tmpSettings);
|
|
590
|
+
for (let i = 0; i < tmpSettingsKeys.length; i++)
|
|
591
|
+
{
|
|
592
|
+
let tmpKey = tmpSettingsKeys[i];
|
|
593
|
+
let tmpVal = tmpSettings[tmpKey];
|
|
594
|
+
|
|
595
|
+
if (typeof(tmpVal) === 'string' && tmpVal.indexOf('{~') >= 0)
|
|
596
|
+
{
|
|
597
|
+
tmpSettings[tmpKey] = this._resolveTemplate(tmpVal, tmpTemplateContext);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return tmpSettings;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ====================================================================
|
|
605
|
+
// Internal: Graph Traversal Helpers
|
|
606
|
+
// ====================================================================
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Build a lookup map of nodes keyed by Hash.
|
|
610
|
+
*/
|
|
611
|
+
_buildNodeMap(pGraph)
|
|
612
|
+
{
|
|
613
|
+
let tmpMap = {};
|
|
614
|
+
let tmpNodes = pGraph.Nodes || [];
|
|
615
|
+
|
|
616
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
617
|
+
{
|
|
618
|
+
let tmpNode = tmpNodes[i];
|
|
619
|
+
|
|
620
|
+
// Normalize flow editor format: "Data" -> "Settings"
|
|
621
|
+
if (!tmpNode.Settings && tmpNode.Data && typeof(tmpNode.Data) === 'object')
|
|
622
|
+
{
|
|
623
|
+
tmpNode.Settings = tmpNode.Data;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
tmpMap[tmpNode.Hash] = tmpNode;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return tmpMap;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Build connection lookup maps for fast traversal.
|
|
634
|
+
* Creates two indices:
|
|
635
|
+
* eventSources[sourceNodeHash] -> array of connections with ConnectionType='Event'
|
|
636
|
+
* stateTargets[targetNodeHash] -> array of connections with ConnectionType='State'
|
|
637
|
+
*
|
|
638
|
+
* When ConnectionType is not explicitly set (flow editor format), the type is
|
|
639
|
+
* inferred from port hash convention (-eo-/-ei- vs -so-/-si-), node types
|
|
640
|
+
* (start/end are always event), or task type definitions.
|
|
641
|
+
*/
|
|
642
|
+
_buildConnectionMap(pGraph, pNodeMap, pPortLabelMap)
|
|
643
|
+
{
|
|
644
|
+
let tmpMap = {
|
|
645
|
+
eventSources: {},
|
|
646
|
+
stateTargets: {}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
let tmpConnections = pGraph.Connections || [];
|
|
650
|
+
|
|
651
|
+
for (let i = 0; i < tmpConnections.length; i++)
|
|
652
|
+
{
|
|
653
|
+
let tmpConn = tmpConnections[i];
|
|
654
|
+
|
|
655
|
+
// Determine connection type (explicit or inferred)
|
|
656
|
+
let tmpType = tmpConn.ConnectionType
|
|
657
|
+
|| this._inferConnectionType(tmpConn, pNodeMap, pPortLabelMap);
|
|
658
|
+
|
|
659
|
+
if (tmpType === 'Event')
|
|
660
|
+
{
|
|
661
|
+
if (!tmpMap.eventSources[tmpConn.SourceNodeHash])
|
|
662
|
+
{
|
|
663
|
+
tmpMap.eventSources[tmpConn.SourceNodeHash] = [];
|
|
664
|
+
}
|
|
665
|
+
tmpMap.eventSources[tmpConn.SourceNodeHash].push(tmpConn);
|
|
666
|
+
}
|
|
667
|
+
else if (tmpType === 'State')
|
|
668
|
+
{
|
|
669
|
+
if (!tmpMap.stateTargets[tmpConn.TargetNodeHash])
|
|
670
|
+
{
|
|
671
|
+
tmpMap.stateTargets[tmpConn.TargetNodeHash] = [];
|
|
672
|
+
}
|
|
673
|
+
tmpMap.stateTargets[tmpConn.TargetNodeHash].push(tmpConn);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return tmpMap;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Find the Start node in the graph.
|
|
682
|
+
*/
|
|
683
|
+
_findStartNode(pContext)
|
|
684
|
+
{
|
|
685
|
+
let tmpNodes = pContext._Graph.Nodes || [];
|
|
686
|
+
|
|
687
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
688
|
+
{
|
|
689
|
+
if (tmpNodes[i].Type === 'start')
|
|
690
|
+
{
|
|
691
|
+
return tmpNodes[i];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Enqueue downstream event connections from a source node's output event port.
|
|
700
|
+
*
|
|
701
|
+
* @param {string} pSourceNodeHash - The node firing the event.
|
|
702
|
+
* @param {string} pEventName - The event name (matches the source port name).
|
|
703
|
+
* @param {object} pContext - The execution context.
|
|
704
|
+
*/
|
|
705
|
+
/**
|
|
706
|
+
* Enqueue ALL downstream event connections from a source node, ignoring port name.
|
|
707
|
+
* Used for Start nodes which have a single output port.
|
|
708
|
+
*/
|
|
709
|
+
_enqueueAllDownstreamEvents(pSourceNodeHash, pContext)
|
|
710
|
+
{
|
|
711
|
+
let tmpConnections = pContext._ConnectionMap.eventSources[pSourceNodeHash] || [];
|
|
712
|
+
let tmpPortLabelMap = pContext._PortLabelMap;
|
|
713
|
+
|
|
714
|
+
for (let i = 0; i < tmpConnections.length; i++)
|
|
715
|
+
{
|
|
716
|
+
let tmpConn = tmpConnections[i];
|
|
717
|
+
let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
|
|
718
|
+
pContext.PendingEvents.push({
|
|
719
|
+
TargetNodeHash: tmpConn.TargetNodeHash,
|
|
720
|
+
EventName: tmpTargetPortName
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
_enqueueDownstreamEvents(pSourceNodeHash, pEventName, pContext)
|
|
726
|
+
{
|
|
727
|
+
let tmpConnections = pContext._ConnectionMap.eventSources[pSourceNodeHash] || [];
|
|
728
|
+
let tmpPortLabelMap = pContext._PortLabelMap;
|
|
729
|
+
|
|
730
|
+
for (let i = 0; i < tmpConnections.length; i++)
|
|
731
|
+
{
|
|
732
|
+
let tmpConn = tmpConnections[i];
|
|
733
|
+
let tmpSourcePortName = this._extractPortName(tmpConn.SourcePortHash, tmpPortLabelMap);
|
|
734
|
+
|
|
735
|
+
if (tmpSourcePortName === pEventName)
|
|
736
|
+
{
|
|
737
|
+
let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
|
|
738
|
+
pContext.PendingEvents.push({
|
|
739
|
+
TargetNodeHash: tmpConn.TargetNodeHash,
|
|
740
|
+
EventName: tmpTargetPortName
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Get downstream event targets for an intermediate event (for sub-graph execution).
|
|
748
|
+
* Returns the targets directly instead of enqueuing them.
|
|
749
|
+
*/
|
|
750
|
+
_getDownstreamEvents(pSourceNodeHash, pEventName, pContext)
|
|
751
|
+
{
|
|
752
|
+
let tmpTargets = [];
|
|
753
|
+
let tmpConnections = pContext._ConnectionMap.eventSources[pSourceNodeHash] || [];
|
|
754
|
+
let tmpPortLabelMap = pContext._PortLabelMap;
|
|
755
|
+
|
|
756
|
+
for (let i = 0; i < tmpConnections.length; i++)
|
|
757
|
+
{
|
|
758
|
+
let tmpConn = tmpConnections[i];
|
|
759
|
+
let tmpSourcePortName = this._extractPortName(tmpConn.SourcePortHash, tmpPortLabelMap);
|
|
760
|
+
|
|
761
|
+
if (tmpSourcePortName === pEventName)
|
|
762
|
+
{
|
|
763
|
+
let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
|
|
764
|
+
tmpTargets.push({
|
|
765
|
+
TargetNodeHash: tmpConn.TargetNodeHash,
|
|
766
|
+
EventName: tmpTargetPortName
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return tmpTargets;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Drain any events that were enqueued during sub-graph execution.
|
|
776
|
+
* This ensures intermediate event processing completes before
|
|
777
|
+
* the parent task continues.
|
|
778
|
+
*/
|
|
779
|
+
_drainEventsForSubgraph(pContext, fCallback)
|
|
780
|
+
{
|
|
781
|
+
if (pContext.PendingEvents.length === 0)
|
|
782
|
+
{
|
|
783
|
+
return fCallback();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
let tmpEvent = pContext.PendingEvents.shift();
|
|
787
|
+
|
|
788
|
+
this._executeTaskForEvent(tmpEvent.TargetNodeHash, tmpEvent.EventName, pContext,
|
|
789
|
+
(pError) =>
|
|
790
|
+
{
|
|
791
|
+
if (pError)
|
|
792
|
+
{
|
|
793
|
+
this._log(pContext, `Error in sub-graph drain: ${pError.message}`, 'error');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
this._drainEventsForSubgraph(pContext, fCallback);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Extract the port name from a port hash.
|
|
802
|
+
*
|
|
803
|
+
* Supports two formats:
|
|
804
|
+
* 1. Programmatic: {NodeHash}-{portTypePrefix}-{Name}
|
|
805
|
+
* e.g. 'TSK-READFILE-001-eo-ReadComplete' -> 'ReadComplete'
|
|
806
|
+
* 2. Flow editor: arbitrary hashes with port Labels stored in the graph
|
|
807
|
+
* e.g. 'fp-read-done' -> looks up Label 'ReadComplete' from PortLabelMap
|
|
808
|
+
*
|
|
809
|
+
* @param {string} pPortHash - The port hash string.
|
|
810
|
+
* @param {object} [pPortLabelMap] - Optional mapping of port hash -> label.
|
|
811
|
+
*/
|
|
812
|
+
_extractPortName(pPortHash, pPortLabelMap)
|
|
813
|
+
{
|
|
814
|
+
if (!pPortHash || typeof(pPortHash) !== 'string')
|
|
815
|
+
{
|
|
816
|
+
return '';
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// First try the standard -eo-/-ei-/-so-/-si- convention
|
|
820
|
+
let tmpPrefixes = ['-ei-', '-eo-', '-si-', '-so-'];
|
|
821
|
+
|
|
822
|
+
for (let i = 0; i < tmpPrefixes.length; i++)
|
|
823
|
+
{
|
|
824
|
+
let tmpIndex = pPortHash.lastIndexOf(tmpPrefixes[i]);
|
|
825
|
+
if (tmpIndex > -1)
|
|
826
|
+
{
|
|
827
|
+
return pPortHash.substring(tmpIndex + tmpPrefixes[i].length);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Then try the port label map (flow editor format)
|
|
832
|
+
if (pPortLabelMap && pPortLabelMap[pPortHash])
|
|
833
|
+
{
|
|
834
|
+
return pPortLabelMap[pPortHash];
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Fallback: return the hash as-is
|
|
838
|
+
return pPortHash;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ====================================================================
|
|
842
|
+
// Internal: Template Resolution
|
|
843
|
+
// ====================================================================
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Resolve a template string against a template context object.
|
|
847
|
+
*
|
|
848
|
+
* Uses Pict's parseTemplate for full template support. The template context
|
|
849
|
+
* from StateManager.buildTemplateContext() is passed as the record parameter,
|
|
850
|
+
* which Pict places at `Record` on the root data object.
|
|
851
|
+
*
|
|
852
|
+
* Template addresses use the `Record.` prefix to reach the context:
|
|
853
|
+
* {~D:Record.Value~} -> the source value from the state connection
|
|
854
|
+
* {~D:Record.Global.X~} -> GlobalState.X
|
|
855
|
+
* {~D:Record.Operation.X~} -> OperationState.X
|
|
856
|
+
* {~D:Record.TaskOutput.NodeHash.X~} -> TaskOutputs[NodeHash].X
|
|
857
|
+
* {~D:Record.Staging.Path~} -> StagingPath
|
|
858
|
+
*
|
|
859
|
+
* @param {string} pTemplate - The template string.
|
|
860
|
+
* @param {object} pContext - The template context (from StateManager.buildTemplateContext).
|
|
861
|
+
* @returns {string} The resolved string.
|
|
862
|
+
*/
|
|
863
|
+
_resolveTemplate(pTemplate, pContext)
|
|
864
|
+
{
|
|
865
|
+
if (typeof(this.fable.parseTemplate) === 'function')
|
|
866
|
+
{
|
|
867
|
+
return this.fable.parseTemplate(pTemplate, pContext);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
this.log.warn('ExecutionEngine._resolveTemplate: parseTemplate not available on fable instance. Template expressions will not be resolved.');
|
|
871
|
+
return pTemplate;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ====================================================================
|
|
875
|
+
// Internal: Port and Connection Helpers
|
|
876
|
+
// ====================================================================
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Build a lookup map from port hash -> port Label for all nodes in the graph.
|
|
880
|
+
* Used to resolve port names when hashes don't follow the -eo-/-ei- convention.
|
|
881
|
+
*/
|
|
882
|
+
_buildPortLabelMap(pGraph)
|
|
883
|
+
{
|
|
884
|
+
let tmpMap = {};
|
|
885
|
+
let tmpNodes = pGraph.Nodes || [];
|
|
886
|
+
|
|
887
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
888
|
+
{
|
|
889
|
+
let tmpPorts = tmpNodes[i].Ports || [];
|
|
890
|
+
|
|
891
|
+
for (let j = 0; j < tmpPorts.length; j++)
|
|
892
|
+
{
|
|
893
|
+
tmpMap[tmpPorts[j].Hash] = tmpPorts[j].Label || tmpPorts[j].Hash;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return tmpMap;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Infer the ConnectionType when not explicitly set on a connection.
|
|
902
|
+
*
|
|
903
|
+
* Uses these heuristics in order:
|
|
904
|
+
* 1. Port hash convention: -eo-/-ei- -> Event, -so-/-si- -> State
|
|
905
|
+
* 2. Start/End node connections are always Event
|
|
906
|
+
* 3. Task type definitions: check if port labels match EventOutputs or StateOutputs
|
|
907
|
+
* 4. Default: Event
|
|
908
|
+
*/
|
|
909
|
+
_inferConnectionType(pConn, pNodeMap, pPortLabelMap)
|
|
910
|
+
{
|
|
911
|
+
let tmpSourcePortHash = pConn.SourcePortHash || '';
|
|
912
|
+
let tmpTargetPortHash = pConn.TargetPortHash || '';
|
|
913
|
+
|
|
914
|
+
// Check port hash convention
|
|
915
|
+
if (tmpSourcePortHash.includes('-eo-') || tmpTargetPortHash.includes('-ei-'))
|
|
916
|
+
{
|
|
917
|
+
return 'Event';
|
|
918
|
+
}
|
|
919
|
+
if (tmpSourcePortHash.includes('-so-') || tmpTargetPortHash.includes('-si-'))
|
|
920
|
+
{
|
|
921
|
+
return 'State';
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Start and End nodes only have event ports
|
|
925
|
+
let tmpSourceNode = pNodeMap ? pNodeMap[pConn.SourceNodeHash] : null;
|
|
926
|
+
let tmpTargetNode = pNodeMap ? pNodeMap[pConn.TargetNodeHash] : null;
|
|
927
|
+
|
|
928
|
+
if (tmpSourceNode && (tmpSourceNode.Type === 'start' || tmpSourceNode.Type === 'end'))
|
|
929
|
+
{
|
|
930
|
+
return 'Event';
|
|
931
|
+
}
|
|
932
|
+
if (tmpTargetNode && (tmpTargetNode.Type === 'start' || tmpTargetNode.Type === 'end'))
|
|
933
|
+
{
|
|
934
|
+
return 'Event';
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Look up port labels and check against task type definitions
|
|
938
|
+
let tmpRegistry = this._getTaskTypeRegistry();
|
|
939
|
+
|
|
940
|
+
if (tmpRegistry && tmpSourceNode)
|
|
941
|
+
{
|
|
942
|
+
let tmpSourceLabel = pPortLabelMap ? (pPortLabelMap[tmpSourcePortHash] || '') : '';
|
|
943
|
+
let tmpDefHash = tmpSourceNode.DefinitionHash || tmpSourceNode.Type;
|
|
944
|
+
let tmpDef = tmpRegistry.getDefinition(tmpDefHash);
|
|
945
|
+
|
|
946
|
+
if (tmpDef)
|
|
947
|
+
{
|
|
948
|
+
if (tmpDef.EventOutputs)
|
|
949
|
+
{
|
|
950
|
+
for (let i = 0; i < tmpDef.EventOutputs.length; i++)
|
|
951
|
+
{
|
|
952
|
+
if (tmpDef.EventOutputs[i].Name === tmpSourceLabel)
|
|
953
|
+
{
|
|
954
|
+
return 'Event';
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (tmpDef.StateOutputs)
|
|
959
|
+
{
|
|
960
|
+
for (let i = 0; i < tmpDef.StateOutputs.length; i++)
|
|
961
|
+
{
|
|
962
|
+
if (tmpDef.StateOutputs[i].Name === tmpSourceLabel)
|
|
963
|
+
{
|
|
964
|
+
return 'State';
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Also check target node to classify
|
|
972
|
+
if (tmpRegistry && tmpTargetNode)
|
|
973
|
+
{
|
|
974
|
+
let tmpTargetLabel = pPortLabelMap ? (pPortLabelMap[tmpTargetPortHash] || '') : '';
|
|
975
|
+
let tmpDefHash = tmpTargetNode.DefinitionHash || tmpTargetNode.Type;
|
|
976
|
+
let tmpDef = tmpRegistry.getDefinition(tmpDefHash);
|
|
977
|
+
|
|
978
|
+
if (tmpDef)
|
|
979
|
+
{
|
|
980
|
+
if (tmpDef.EventInputs)
|
|
981
|
+
{
|
|
982
|
+
for (let i = 0; i < tmpDef.EventInputs.length; i++)
|
|
983
|
+
{
|
|
984
|
+
if (tmpDef.EventInputs[i].Name === tmpTargetLabel)
|
|
985
|
+
{
|
|
986
|
+
return 'Event';
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (tmpDef.SettingsInputs)
|
|
991
|
+
{
|
|
992
|
+
for (let i = 0; i < tmpDef.SettingsInputs.length; i++)
|
|
993
|
+
{
|
|
994
|
+
if (tmpDef.SettingsInputs[i].Name === tmpTargetLabel)
|
|
995
|
+
{
|
|
996
|
+
return 'State';
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Default to Event
|
|
1004
|
+
return 'Event';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ====================================================================
|
|
1008
|
+
// Internal: Service Access
|
|
1009
|
+
// ====================================================================
|
|
1010
|
+
|
|
1011
|
+
_getStateManager()
|
|
1012
|
+
{
|
|
1013
|
+
if (this.fable.servicesMap['UltravisorStateManager'])
|
|
1014
|
+
{
|
|
1015
|
+
return Object.values(this.fable.servicesMap['UltravisorStateManager'])[0];
|
|
1016
|
+
}
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
_getTaskTypeRegistry()
|
|
1021
|
+
{
|
|
1022
|
+
if (this.fable.servicesMap['UltravisorTaskTypeRegistry'])
|
|
1023
|
+
{
|
|
1024
|
+
return Object.values(this.fable.servicesMap['UltravisorTaskTypeRegistry'])[0];
|
|
1025
|
+
}
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
_getManifestService()
|
|
1030
|
+
{
|
|
1031
|
+
if (this.fable.servicesMap['UltravisorExecutionManifest'])
|
|
1032
|
+
{
|
|
1033
|
+
return Object.values(this.fable.servicesMap['UltravisorExecutionManifest'])[0];
|
|
1034
|
+
}
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
module.exports = UltravisorExecutionEngine;
|