@maxdrellin/xenocline 0.0.6 → 0.0.8
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/dist/aggregator.d.ts +25 -0
- package/dist/aggregator.js.map +1 -1
- package/dist/execution/aggregator.d.ts +1 -1
- package/dist/execution/aggregator.js.map +1 -1
- package/dist/execution/next.js +35 -14
- package/dist/execution/next.js.map +1 -1
- package/dist/execution/node.js +18 -4
- package/dist/execution/node.js.map +1 -1
- package/dist/execution/process.js +19 -1
- package/dist/execution/process.js.map +1 -1
- package/dist/node/phasenode.d.ts +22 -0
- package/dist/node/phasenode.js.map +1 -1
- package/dist/transition/beginning.js.map +1 -1
- package/dist/transition/connection.d.ts +29 -0
- package/dist/transition/connection.js +11 -1
- package/dist/transition/connection.js.map +1 -1
- package/dist/xenocline.cjs +83 -20
- package/dist/xenocline.cjs.map +1 -1
- package/package.json +5 -7
- package/scripts/pre-commit-hook.sh +1 -0
|
@@ -2,15 +2,44 @@ import { Input } from '../input';
|
|
|
2
2
|
import { Context } from '../context';
|
|
3
3
|
import { Output } from '../output';
|
|
4
4
|
import { Transition } from './transition';
|
|
5
|
+
/**
|
|
6
|
+
* Transform function for connections between nodes.
|
|
7
|
+
* Takes the output from the source node and the current context,
|
|
8
|
+
* and returns the input for the target node along with an updated context.
|
|
9
|
+
*
|
|
10
|
+
* CONCURRENCY WARNING: In parallel execution scenarios (fan-out from one node
|
|
11
|
+
* to multiple target nodes), each connection's transform receives the same
|
|
12
|
+
* source context. Context mutations from one connection may be overwritten
|
|
13
|
+
* by another. The last transform to complete will have its context changes
|
|
14
|
+
* reflected in the process state.
|
|
15
|
+
*
|
|
16
|
+
* Best practice: Minimize context mutations in transforms, or use unique
|
|
17
|
+
* context keys per connection path to avoid conflicts.
|
|
18
|
+
*/
|
|
5
19
|
export type TransformFunction<O extends Output = Output, C extends Context = Context> = (output: O, context: C) => Promise<[Input, C]>;
|
|
6
20
|
export interface Connection<O extends Output = Output, C extends Context = Context> extends Transition {
|
|
7
21
|
type: 'connection';
|
|
8
22
|
targetNodeId: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional function to transform the output of the current phase
|
|
25
|
+
* to the input of the target phase.
|
|
26
|
+
* If not provided, the output is assumed to be compatible directly.
|
|
27
|
+
*
|
|
28
|
+
* See TransformFunction documentation for concurrency considerations.
|
|
29
|
+
*/
|
|
9
30
|
transform: TransformFunction<O, C>;
|
|
10
31
|
}
|
|
11
32
|
export interface ConnectionOptions<O extends Output = Output, C extends Context = Context> {
|
|
12
33
|
transform: TransformFunction<O, C>;
|
|
13
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Default connection options with a pass-through transform.
|
|
37
|
+
* WARNING: The default transform uses a type assertion (output as Input) which provides
|
|
38
|
+
* no runtime validation. Users should provide their own transform function if the output
|
|
39
|
+
* type does not structurally match the expected input type of the target node.
|
|
40
|
+
* The type assertion is used here to maintain backward compatibility and allow simple
|
|
41
|
+
* pass-through scenarios where Output and Input have compatible shapes.
|
|
42
|
+
*/
|
|
14
43
|
export declare const DEFAULT_CONNECTION_OPTIONS: ConnectionOptions;
|
|
15
44
|
export declare const createConnection: <O extends Output = Output, C extends Context = Context>(id: string, targetNodeId: string, options?: Partial<ConnectionOptions<O, C>>) => Readonly<Connection<O, C>>;
|
|
16
45
|
export declare const isConnection: <O extends Output = Output, C extends Context = Context>(item: any) => item is Connection<O, C>;
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { createTransition, isTransition, validateTransition } from './transition.js';
|
|
2
2
|
import { clean } from '../util/general.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Default connection options with a pass-through transform.
|
|
6
|
+
* WARNING: The default transform uses a type assertion (output as Input) which provides
|
|
7
|
+
* no runtime validation. Users should provide their own transform function if the output
|
|
8
|
+
* type does not structurally match the expected input type of the target node.
|
|
9
|
+
* The type assertion is used here to maintain backward compatibility and allow simple
|
|
10
|
+
* pass-through scenarios where Output and Input have compatible shapes.
|
|
11
|
+
*/ const DEFAULT_CONNECTION_OPTIONS = {
|
|
5
12
|
transform: async (output, context)=>{
|
|
13
|
+
// Type assertion used here - Output and Input are both extensible objects
|
|
14
|
+
// so this is safe for pass-through scenarios, but users should validate
|
|
15
|
+
// or transform data if types don't align
|
|
6
16
|
return [
|
|
7
17
|
output,
|
|
8
18
|
context
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","sources":["../../src/transition/connection.ts"],"sourcesContent":["import { Input } from '../input';\nimport { Context } from '../context';\nimport { Output } from '../output';\nimport { createTransition, isTransition, Transition, validateTransition } from './transition';\nimport { clean } from '../util/general';\n\nexport type TransformFunction<O extends Output = Output, C extends Context = Context> = (output: O, context: C) => Promise<[Input, C]>;\n\n// MODIFIED: Connection to extend Transition\nexport interface Connection<\n O extends Output = Output,\n C extends Context = Context,\n> extends Transition {\n type: 'connection';\n targetNodeId: string; // ID of the target PhaseNode in the process's phases collection\n
|
|
1
|
+
{"version":3,"file":"connection.js","sources":["../../src/transition/connection.ts"],"sourcesContent":["import { Input } from '../input';\nimport { Context } from '../context';\nimport { Output } from '../output';\nimport { createTransition, isTransition, Transition, validateTransition } from './transition';\nimport { clean } from '../util/general';\n\n/**\n * Transform function for connections between nodes.\n * Takes the output from the source node and the current context,\n * and returns the input for the target node along with an updated context.\n *\n * CONCURRENCY WARNING: In parallel execution scenarios (fan-out from one node\n * to multiple target nodes), each connection's transform receives the same\n * source context. Context mutations from one connection may be overwritten\n * by another. The last transform to complete will have its context changes\n * reflected in the process state.\n *\n * Best practice: Minimize context mutations in transforms, or use unique\n * context keys per connection path to avoid conflicts.\n */\nexport type TransformFunction<O extends Output = Output, C extends Context = Context> = (output: O, context: C) => Promise<[Input, C]>;\n\n// MODIFIED: Connection to extend Transition\nexport interface Connection<\n O extends Output = Output,\n C extends Context = Context,\n> extends Transition {\n type: 'connection';\n targetNodeId: string; // ID of the target PhaseNode in the process's phases collection\n /**\n * Optional function to transform the output of the current phase\n * to the input of the target phase.\n * If not provided, the output is assumed to be compatible directly.\n *\n * See TransformFunction documentation for concurrency considerations.\n */\n transform: TransformFunction<O, C>;\n}\n\nexport interface ConnectionOptions<O extends Output = Output, C extends Context = Context> {\n transform: TransformFunction<O, C>;\n}\n\n/**\n * Default connection options with a pass-through transform.\n * WARNING: The default transform uses a type assertion (output as Input) which provides\n * no runtime validation. Users should provide their own transform function if the output\n * type does not structurally match the expected input type of the target node.\n * The type assertion is used here to maintain backward compatibility and allow simple\n * pass-through scenarios where Output and Input have compatible shapes.\n */\nexport const DEFAULT_CONNECTION_OPTIONS: ConnectionOptions = {\n transform: async (output, context) => {\n // Type assertion used here - Output and Input are both extensible objects\n // so this is safe for pass-through scenarios, but users should validate\n // or transform data if types don't align\n return [output as Input, context];\n }\n};\n\nexport const createConnection = <O extends Output = Output, C extends Context = Context>(\n id: string,\n targetNodeId: string,\n options?: Partial<ConnectionOptions<O, C>>\n): Readonly<Connection<O, C>> => {\n\n let connectionOptions: ConnectionOptions<O, C> = { ...DEFAULT_CONNECTION_OPTIONS } as unknown as ConnectionOptions<O, C>;\n\n if (options) {\n connectionOptions = { ...connectionOptions, ...clean(options) };\n }\n\n return {\n ...createTransition('connection', id),\n targetNodeId,\n transform: connectionOptions.transform,\n } as Connection<O, C>;\n};\n\nexport const isConnection = <O extends Output = Output, C extends Context = Context>(item: any): item is Connection<O, C> => {\n return isTransition(item) && item.type === 'connection' && (item as Connection<O, C>).targetNodeId !== undefined;\n};\n\nexport const validateConnection = (\n item: any,\n coordinates?: string[]\n): Array<{ coordinates: string[], error: string }> => {\n const errors: Array<{ coordinates: string[], error: string }> = [];\n const connectionBaseCoordinates = [...(coordinates || []), 'Connection'];\n\n errors.push(...validateTransition(item, coordinates));\n\n if (errors.length === 0) {\n if (item && typeof item === 'object') {\n const connectionSpecificErrorPath = [...connectionBaseCoordinates, `Connection: ${item.id}`];\n\n if (item.type === 'connection') {\n if (typeof item.targetNodeId !== 'string') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"targetNodeId\" must be a string when type is \"connection\".' });\n }\n // transform is optional, but if present, must be a function.\n if (item.transform !== undefined && typeof item.transform !== 'function') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Optional property \"transform\" must be a function if present.' });\n }\n } else {\n // If type is not 'connection', but these properties exist and are malformed.\n // This primarily helps catch if a non-connection object has these fields incorrectly defined.\n if (item.targetNodeId !== undefined && typeof item.targetNodeId !== 'string') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"targetNodeId\" is present but is not a string.' });\n }\n if (item.transform !== undefined && typeof item.transform !== 'function') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"transform\" is present but is not a function.' });\n }\n }\n }\n }\n return errors;\n};\n\n/**\n * Event emitted specifically for connection transitions.\n */\nexport interface ConnectionEvent extends TransitionEvent {\n transitionType: 'connection';\n}\n"],"names":["DEFAULT_CONNECTION_OPTIONS","transform","output","context","createConnection","id","targetNodeId","options","connectionOptions","clean","createTransition","isConnection","item","isTransition","type","undefined","validateConnection","coordinates","errors","connectionBaseCoordinates","push","validateTransition","length","connectionSpecificErrorPath","error"],"mappings":";;;AA2CA;;;;;;;UAQaA,0BAAAA,GAAgD;AACzDC,IAAAA,SAAAA,EAAW,OAAOC,MAAAA,EAAQC,OAAAA,GAAAA;;;;QAItB,OAAO;AAACD,YAAAA,MAAAA;AAAiBC,YAAAA;AAAQ,SAAA;AACrC,IAAA;AACJ;AAEO,MAAMC,gBAAAA,GAAmB,CAC5BC,EAAAA,EACAC,YAAAA,EACAC,OAAAA,GAAAA;AAGA,IAAA,IAAIC,iBAAAA,GAA6C;AAAE,QAAA,GAAGR;AAA2B,KAAA;AAEjF,IAAA,IAAIO,OAAAA,EAAS;QACTC,iBAAAA,GAAoB;AAAE,YAAA,GAAGA,iBAAiB;AAAE,YAAA,GAAGC,MAAMF,OAAAA;AAAS,SAAA;AAClE,IAAA;IAEA,OAAO;QACH,GAAGG,gBAAAA,CAAiB,cAAcL,EAAAA,CAAG;AACrCC,QAAAA,YAAAA;AACAL,QAAAA,SAAAA,EAAWO,kBAAkBP;AACjC,KAAA;AACJ;AAEO,MAAMU,eAAe,CAAyDC,IAAAA,GAAAA;IACjF,OAAOC,YAAAA,CAAaD,SAASA,IAAAA,CAAKE,IAAI,KAAK,YAAA,IAAiBF,IAAAA,CAA0BN,YAAY,KAAKS,SAAAA;AAC3G;AAEO,MAAMC,kBAAAA,GAAqB,CAC9BJ,IAAAA,EACAK,WAAAA,GAAAA;AAEA,IAAA,MAAMC,SAA0D,EAAE;AAClE,IAAA,MAAMC,yBAAAA,GAA4B;AAAKF,QAAAA,GAAAA,WAAAA,IAAe,EAAE;AAAG,QAAA;AAAa,KAAA;IAExEC,MAAAA,CAAOE,IAAI,CAAA,GAAIC,kBAAAA,CAAmBT,IAAAA,EAAMK,WAAAA,CAAAA,CAAAA;IAExC,IAAIC,MAAAA,CAAOI,MAAM,KAAK,CAAA,EAAG;QACrB,IAAIV,IAAAA,IAAQ,OAAOA,IAAAA,KAAS,QAAA,EAAU;AAClC,YAAA,MAAMW,2BAAAA,GAA8B;AAAIJ,gBAAAA,GAAAA,yBAAAA;AAA2B,gBAAA,CAAC,YAAY,EAAEP,IAAAA,CAAKP,EAAE,CAAA;AAAG,aAAA;YAE5F,IAAIO,IAAAA,CAAKE,IAAI,KAAK,YAAA,EAAc;AAC5B,gBAAA,IAAI,OAAOF,IAAAA,CAAKN,YAAY,KAAK,QAAA,EAAU;AACvCY,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAAsE,qBAAA,CAAA;AACzI,gBAAA;;gBAEA,IAAIZ,IAAAA,CAAKX,SAAS,KAAKc,SAAAA,IAAa,OAAOH,IAAAA,CAAKX,SAAS,KAAK,UAAA,EAAY;AACtEiB,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAA+D,qBAAA,CAAA;AAClI,gBAAA;YACJ,CAAA,MAAO;;;gBAGH,IAAIZ,IAAAA,CAAKN,YAAY,KAAKS,SAAAA,IAAa,OAAOH,IAAAA,CAAKN,YAAY,KAAK,QAAA,EAAU;AAC1EY,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAA0D,qBAAA,CAAA;AAC7H,gBAAA;gBACA,IAAIZ,IAAAA,CAAKX,SAAS,KAAKc,SAAAA,IAAa,OAAOH,IAAAA,CAAKX,SAAS,KAAK,UAAA,EAAY;AACtEiB,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAAyD,qBAAA,CAAA;AAC5H,gBAAA;AACJ,YAAA;AACJ,QAAA;AACJ,IAAA;IACA,OAAON,MAAAA;AACX;;;;"}
|
package/dist/xenocline.cjs
CHANGED
|
@@ -183,8 +183,18 @@ const validateTermination = (item, coordinates)=>{
|
|
|
183
183
|
return errors;
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Default connection options with a pass-through transform.
|
|
188
|
+
* WARNING: The default transform uses a type assertion (output as Input) which provides
|
|
189
|
+
* no runtime validation. Users should provide their own transform function if the output
|
|
190
|
+
* type does not structurally match the expected input type of the target node.
|
|
191
|
+
* The type assertion is used here to maintain backward compatibility and allow simple
|
|
192
|
+
* pass-through scenarios where Output and Input have compatible shapes.
|
|
193
|
+
*/ const DEFAULT_CONNECTION_OPTIONS = {
|
|
187
194
|
transform: async (output, context)=>{
|
|
195
|
+
// Type assertion used here - Output and Input are both extensible objects
|
|
196
|
+
// so this is safe for pass-through scenarios, but users should validate
|
|
197
|
+
// or transform data if types don't align
|
|
188
198
|
return [
|
|
189
199
|
output,
|
|
190
200
|
context
|
|
@@ -883,17 +893,26 @@ async function handleNextStep(nodeOutput, nodeId, next, state) {
|
|
|
883
893
|
await handleNextStep(nodeOutput, decision.id, decisionOutcome, state); // outcome processed, decision.id is context for next step if it's an error source. The original nodeId is implicitly the source of this decision.
|
|
884
894
|
await dispatchEvent(state.eventState, createDecisionEvent(nodeId, 'end', decision), state.context);
|
|
885
895
|
} catch (decisionError) {
|
|
896
|
+
const errorMessage = `Decision error on '${decision.id}' from node '${nodeId}': ${decisionError.message}`;
|
|
886
897
|
// eslint-disable-next-line no-console
|
|
887
|
-
console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR]
|
|
898
|
+
console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR]`, {
|
|
899
|
+
error: errorMessage,
|
|
888
900
|
decisionError,
|
|
889
|
-
|
|
890
|
-
|
|
901
|
+
decisionId: decision.id,
|
|
902
|
+
sourceNodeId: nodeId
|
|
891
903
|
});
|
|
892
904
|
state.errors.push({
|
|
893
905
|
nodeId: decision.id,
|
|
894
|
-
message:
|
|
906
|
+
message: errorMessage,
|
|
907
|
+
details: {
|
|
908
|
+
sourceNodeId: nodeId,
|
|
909
|
+
originalError: decisionError.message
|
|
910
|
+
}
|
|
895
911
|
});
|
|
896
|
-
// Note:
|
|
912
|
+
// Note: Decision errors are logged but do not halt the process.
|
|
913
|
+
// This allows other parallel decisions to continue executing.
|
|
914
|
+
// If you need decision errors to halt execution, consider throwing here
|
|
915
|
+
// or checking state.errors after process completion.
|
|
897
916
|
}
|
|
898
917
|
}
|
|
899
918
|
} else if (Array.isArray(next) && next.length > 0 && next.every(isConnection)) {
|
|
@@ -919,15 +938,24 @@ async function handleNextStep(nodeOutput, nodeId, next, state) {
|
|
|
919
938
|
state.context = nextContext;
|
|
920
939
|
//console.log('[_HANDLE_NEXT_STEP_CONNECTION_TRANSFORM_SUCCESS]', { nodeId, targetNodeId: connection.targetNodeId, nextInput, nextContext });
|
|
921
940
|
} catch (transformError) {
|
|
941
|
+
const errorMessage = `Transform error on connection '${connection.id}' from node '${nodeId}' to '${connection.targetNodeId}': ${transformError.message}`;
|
|
922
942
|
// eslint-disable-next-line no-console
|
|
923
|
-
console.error(`[_HANDLE_NEXT_STEP_CONNECTION_TRANSFORM_ERROR]
|
|
943
|
+
console.error(`[_HANDLE_NEXT_STEP_CONNECTION_TRANSFORM_ERROR]`, {
|
|
944
|
+
error: errorMessage,
|
|
924
945
|
transformError,
|
|
925
|
-
|
|
946
|
+
connectionId: connection.id,
|
|
947
|
+
sourceNodeId: nodeId,
|
|
926
948
|
targetNodeId: connection.targetNodeId
|
|
927
949
|
});
|
|
950
|
+
// Store error with connection ID for better traceability
|
|
928
951
|
state.errors.push({
|
|
929
|
-
nodeId: connection.
|
|
930
|
-
message:
|
|
952
|
+
nodeId: connection.id,
|
|
953
|
+
message: errorMessage,
|
|
954
|
+
details: {
|
|
955
|
+
sourceNodeId: nodeId,
|
|
956
|
+
targetNodeId: connection.targetNodeId,
|
|
957
|
+
originalError: transformError.message
|
|
958
|
+
}
|
|
931
959
|
});
|
|
932
960
|
continue;
|
|
933
961
|
}
|
|
@@ -947,18 +975,21 @@ async function handleNextStep(nodeOutput, nodeId, next, state) {
|
|
|
947
975
|
const result = nodeOutput;
|
|
948
976
|
if (termination.terminate) {
|
|
949
977
|
//console.log('[_HANDLE_NEXT_STEP_TERMINATION_CALLING_TERMINATE_FN]', { nodeId, terminationId: termination.id });
|
|
950
|
-
termination.terminate(nodeOutput, state.context);
|
|
978
|
+
await termination.terminate(nodeOutput, state.context);
|
|
951
979
|
await dispatchEvent(state.eventState, createTerminationEvent(nodeId, 'terminate', termination, {
|
|
952
980
|
output: nodeOutput
|
|
953
981
|
}), state.context);
|
|
954
982
|
}
|
|
955
983
|
state.results[termination.id] = result;
|
|
956
984
|
} else if (Array.isArray(next) && next.length === 0) {
|
|
957
|
-
// Empty array
|
|
958
|
-
// This
|
|
959
|
-
//
|
|
985
|
+
// Empty array from a Decision means no path should be taken from this node.
|
|
986
|
+
// This is treated as an implicit termination, storing the result with a generated key.
|
|
987
|
+
// Note: This behavior means a Decision can dynamically terminate a process path.
|
|
960
988
|
const result = nodeOutput;
|
|
961
|
-
|
|
989
|
+
const implicitTerminationId = `${nodeId}_implicit_end`;
|
|
990
|
+
state.results[implicitTerminationId] = result;
|
|
991
|
+
// eslint-disable-next-line no-console
|
|
992
|
+
console.warn(`[_HANDLE_NEXT_STEP_IMPLICIT_TERMINATION] Node ${nodeId} received empty next array, treating as implicit termination with id: ${implicitTerminationId}`);
|
|
962
993
|
} else {
|
|
963
994
|
// If there is no next (e.g. next is undefined or null after a decision), or it's an unhandled type.
|
|
964
995
|
// Consider this an end state and store the result with the nodeId
|
|
@@ -1093,15 +1124,21 @@ async function executeNode(nodeId, input, state) {
|
|
|
1093
1124
|
await handleNextStep(output, decision.id, decisionOutcome, state);
|
|
1094
1125
|
dispatchEvent(state.eventState, createDecisionEvent(nodeId, 'end', decision), state.context);
|
|
1095
1126
|
} catch (decisionError) {
|
|
1127
|
+
const errorMessage = `Decision error on '${decision.id}' for node '${nodeId}': ${decisionError.message}`;
|
|
1096
1128
|
// eslint-disable-next-line no-console
|
|
1097
|
-
console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR]
|
|
1129
|
+
console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR]`, {
|
|
1130
|
+
error: errorMessage,
|
|
1098
1131
|
decisionError,
|
|
1099
|
-
|
|
1100
|
-
|
|
1132
|
+
decisionId: decision.id,
|
|
1133
|
+
sourceNodeId: nodeId
|
|
1101
1134
|
});
|
|
1102
1135
|
state.errors.push({
|
|
1103
1136
|
nodeId: decision.id,
|
|
1104
|
-
message:
|
|
1137
|
+
message: errorMessage,
|
|
1138
|
+
details: {
|
|
1139
|
+
sourceNodeId: nodeId,
|
|
1140
|
+
originalError: decisionError.message
|
|
1141
|
+
}
|
|
1105
1142
|
});
|
|
1106
1143
|
}
|
|
1107
1144
|
})();
|
|
@@ -1142,6 +1179,14 @@ async function executeNode(nodeId, input, state) {
|
|
|
1142
1179
|
nodeId,
|
|
1143
1180
|
message: error.message
|
|
1144
1181
|
});
|
|
1182
|
+
// Clean up any pending aggregator deferred on error
|
|
1183
|
+
if (state.aggregatorDeferreds.has(nodeId)) {
|
|
1184
|
+
const deferred = state.aggregatorDeferreds.get(nodeId);
|
|
1185
|
+
if (deferred) {
|
|
1186
|
+
deferred.reject(error);
|
|
1187
|
+
}
|
|
1188
|
+
state.aggregatorDeferreds.delete(nodeId);
|
|
1189
|
+
}
|
|
1145
1190
|
throw error;
|
|
1146
1191
|
} finally{
|
|
1147
1192
|
//console.log('[EXECUTE_NODE_RECURSIVE_IIFE_FINALLY]', { nodeId, hasAggregatorDeferred: state.aggregatorDeferreds.has(nodeId) });
|
|
@@ -1232,13 +1277,31 @@ async function executeProcess(processInstance, beginning, options) {
|
|
|
1232
1277
|
collectedErrors: state.errors
|
|
1233
1278
|
});
|
|
1234
1279
|
}
|
|
1280
|
+
// Check for and reject any pending aggregators that never completed
|
|
1235
1281
|
if (state.aggregatorDeferreds && state.aggregatorDeferreds.size > 0) {
|
|
1236
1282
|
const pendingNodeIds = state.pendingAggregatorIds ? state.pendingAggregatorIds().join(', ') : 'unknown';
|
|
1237
1283
|
// eslint-disable-next-line no-console
|
|
1238
|
-
console.warn(`[EXECUTE_PROCESS_PENDING_AGGREGATORS] Process execution
|
|
1284
|
+
console.warn(`[EXECUTE_PROCESS_PENDING_AGGREGATORS] Process execution completed with pending aggregators: ${pendingNodeIds}. These will be rejected.`, {
|
|
1239
1285
|
processName: processInstance.name,
|
|
1240
1286
|
pendingNodeIds
|
|
1241
1287
|
});
|
|
1288
|
+
// Reject all pending aggregators to prevent hanging promises
|
|
1289
|
+
for (const nodeId of state.aggregatorDeferreds.keys()){
|
|
1290
|
+
const deferred = state.aggregatorDeferreds.get(nodeId);
|
|
1291
|
+
if (deferred) {
|
|
1292
|
+
const error = new Error(`Aggregator node '${nodeId}' did not receive all expected inputs before process completion. This may indicate a process design issue where not all paths leading to the aggregator were executed.`);
|
|
1293
|
+
deferred.reject(error);
|
|
1294
|
+
state.errors.push({
|
|
1295
|
+
nodeId,
|
|
1296
|
+
message: error.message,
|
|
1297
|
+
details: {
|
|
1298
|
+
reason: 'incomplete_aggregation'
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
// Clear the map after rejecting all
|
|
1304
|
+
state.aggregatorDeferreds.clear();
|
|
1242
1305
|
}
|
|
1243
1306
|
dispatchEvent(state.eventState, createProcessEvent(processInstance.name, 'end', processInstance, {
|
|
1244
1307
|
input: processExecutionOptions.input,
|