@finos/legend-studio
Version:
838 lines • 50.5 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { action, computed, flowResult, makeAutoObservable } from 'mobx';
import { CHANGE_DETECTION_EVENT } from './ChangeDetectionEvent.js';
import { GRAPH_EDITOR_MODE, AUX_PANEL_MODE } from './EditorConfig.js';
import { LogEvent, assertType, UnsupportedOperationError, assertErrorThrown, assertTrue, isNonNullable, NetworkClientError, guaranteeNonNullable, StopWatch, filterByType, ActionState, } from '@finos/legend-shared';
import { ElementEditorState } from './editor-state/element-editor-state/ElementEditorState.js';
import { GraphGenerationState } from './editor-state/GraphGenerationState.js';
import { MODEL_UPDATER_INPUT_TYPE } from './editor-state/ModelLoaderState.js';
import { EntityChangeType, ProjectConfiguration, applyEntityChanges, } from '@finos/legend-server-sdlc';
import { ProjectVersionEntities, ProjectData, ProjectDependencyCoordinates, generateGAVCoordinates, } from '@finos/legend-server-depot';
import { GRAPH_MANAGER_EVENT, CompilationError, EngineError, extractSourceInformationCoordinates, Package, PureInstanceSetImplementation, Profile, OperationSetImplementation, PrimitiveType, Enumeration, Class, Association, Mapping, ConcreteFunctionDefinition, Service, FlatData, FlatDataInstanceSetImplementation, EmbeddedFlatDataPropertyMapping, PackageableConnection, PackageableRuntime, FileGenerationSpecification, GenerationSpecification, Measure, Unit, Database, SectionIndex, RootRelationalInstanceSetImplementation, EmbeddedRelationalInstanceSetImplementation, AggregationAwareSetImplementation, DependencyGraphBuilderError, GraphDataDeserializationError, GraphBuilderError, GraphManagerTelemetry, DataElement, } from '@finos/legend-graph';
import { ActionAlertActionType, ActionAlertType, } from '@finos/legend-application';
import { CONFIGURATION_EDITOR_TAB } from './editor-state/ProjectConfigurationEditorState.js';
import { graph_dispose } from './graphModifier/GraphModifierHelper.js';
import { PACKAGEABLE_ELEMENT_TYPE, SET_IMPLEMENTATION_TYPE, } from './shared/ModelUtil.js';
import { GlobalTestRunnerState } from './sidebar-state/testable/GlobalTestRunnerState.js';
export var GraphBuilderStatus;
(function (GraphBuilderStatus) {
GraphBuilderStatus["SUCCEEDED"] = "SUCCEEDED";
GraphBuilderStatus["FAILED"] = "FAILED";
GraphBuilderStatus["REDIRECTED_TO_TEXT_MODE"] = "REDIRECTED_TO_TEXT_MODE";
})(GraphBuilderStatus = GraphBuilderStatus || (GraphBuilderStatus = {}));
export class EditorGraphState {
editorStore;
graphGenerationState;
isInitializingGraph = false;
isRunningGlobalCompile = false;
isRunningGlobalGenerate = false;
isApplicationLeavingTextMode = false;
isUpdatingGraph = false; // critical synchronous update to refresh the graph
isUpdatingApplication = false; // including graph update and async operations such as change detection
constructor(editorStore) {
makeAutoObservable(this, {
editorStore: false,
graphGenerationState: false,
getPackageableElementType: false,
getSetImplementationType: false,
hasCompilationError: computed,
clearCompilationError: action,
});
this.editorStore = editorStore;
this.graphGenerationState = new GraphGenerationState(this.editorStore);
}
get hasCompilationError() {
return (Boolean(this.editorStore.grammarTextEditorState.error) ||
this.editorStore.openedEditorStates
.filter(filterByType(ElementEditorState))
.some((editorState) => editorState.hasCompilationError));
}
clearCompilationError() {
this.editorStore.grammarTextEditorState.setError(undefined);
this.editorStore.openedEditorStates
.filter(filterByType(ElementEditorState))
.forEach((editorState) => editorState.clearCompilationError());
}
get isApplicationUpdateOperationIsRunning() {
return (this.isRunningGlobalCompile ||
this.isRunningGlobalGenerate ||
this.isApplicationLeavingTextMode ||
this.isUpdatingApplication ||
this.isInitializingGraph);
}
checkIfApplicationUpdateOperationIsRunning() {
if (this.isRunningGlobalGenerate) {
this.editorStore.applicationStore.notifyWarning('Please wait for model generation to complete');
return true;
}
if (this.isRunningGlobalCompile) {
this.editorStore.applicationStore.notifyWarning('Please wait for graph compilation to complete');
return true;
}
if (this.isApplicationLeavingTextMode) {
this.editorStore.applicationStore.notifyWarning('Please wait for editor to leave text mode completely');
return true;
}
if (this.isUpdatingApplication) {
this.editorStore.applicationStore.notifyWarning('Please wait for editor state to rebuild');
return true;
}
if (this.isInitializingGraph) {
this.editorStore.applicationStore.notifyWarning('Please wait for editor initialization to complete');
return true;
}
return false;
}
*buildGraph(entities) {
try {
this.isInitializingGraph = true;
const stopWatch = new StopWatch();
// reset
this.editorStore.graphManagerState.resetGraph();
// fetch and build dependencies
stopWatch.record();
const dependencyManager = this.editorStore.graphManagerState.createEmptyDependencyManager();
this.editorStore.graphManagerState.graph.dependencyManager =
dependencyManager;
this.editorStore.graphManagerState.dependenciesBuildState.setMessage(`Fetching dependencies...`);
const dependencyEntitiesIndex = (yield flowResult(this.getIndexedDependencyEntities()));
stopWatch.record(GRAPH_MANAGER_EVENT.GRAPH_DEPENDENCIES_FETCHED);
const dependency_buildReport = (yield this.editorStore.graphManagerState.graphManager.buildDependencies(this.editorStore.graphManagerState.coreModel, this.editorStore.graphManagerState.systemModel, dependencyManager, dependencyEntitiesIndex, this.editorStore.graphManagerState.dependenciesBuildState));
dependency_buildReport.timings[GRAPH_MANAGER_EVENT.GRAPH_DEPENDENCIES_FETCHED] = stopWatch.getRecord(GRAPH_MANAGER_EVENT.GRAPH_DEPENDENCIES_FETCHED);
// build graph
const graph_buildReport = (yield this.editorStore.graphManagerState.graphManager.buildGraph(this.editorStore.graphManagerState.graph, entities, this.editorStore.graphManagerState.graphBuildState, {
TEMPORARY__preserveSectionIndex: this.editorStore.applicationStore.config.options
.TEMPORARY__preserveSectionIndex,
}));
// build generations
const generation_buildReport = (yield this.editorStore.graphManagerState.graphManager.buildGenerations(this.editorStore.graphManagerState.graph, this.graphGenerationState.generatedEntities, this.editorStore.graphManagerState.generationsBuildState));
// report
stopWatch.record(GRAPH_MANAGER_EVENT.GRAPH_INITIALIZED);
const graphBuilderReportData = {
timings: {
[GRAPH_MANAGER_EVENT.GRAPH_INITIALIZED]: stopWatch.getRecord(GRAPH_MANAGER_EVENT.GRAPH_INITIALIZED),
},
dependencies: dependency_buildReport,
graph: graph_buildReport,
generations: generation_buildReport,
};
this.editorStore.applicationStore.log.info(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_INITIALIZED), graphBuilderReportData);
GraphManagerTelemetry.logEvent_GraphInitialized(this.editorStore.applicationStore.telemetryService, graphBuilderReportData);
// add generation specification if model generation elements exists in graph and no generation specification
yield flowResult(this.graphGenerationState.possiblyAddMissingGenerationSpecifications());
return {
status: GraphBuilderStatus.SUCCEEDED,
};
}
catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE), error);
if (error instanceof DependencyGraphBuilderError) {
this.editorStore.graphManagerState.graphBuildState.fail();
// no recovery if dependency models cannot be built, this makes assumption that all dependencies models are compiled successfully
// TODO: we might want to handle this more gracefully when we can show people the dependency model element in the future
this.editorStore.applicationStore.notifyError(`Can't initialize dependency models. Error: ${error.message}`);
const projectConfigurationEditorState = this.editorStore.projectConfigurationEditorState;
projectConfigurationEditorState.setSelectedTab(CONFIGURATION_EDITOR_TAB.PROJECT_DEPENDENCIES);
this.editorStore.setCurrentEditorState(projectConfigurationEditorState);
}
else if (error instanceof GraphDataDeserializationError) {
// if something goes wrong with de-serialization, redirect to model loader to fix
this.redirectToModelLoaderForDebugging(error);
}
else if (error instanceof NetworkClientError) {
this.editorStore.graphManagerState.graphBuildState.fail();
this.editorStore.applicationStore.notifyWarning(`Can't build graph. Error: ${error.message}`);
}
else {
// TODO: we should split this into 2 notifications when we support multiple notifications
this.editorStore.applicationStore.notifyError(`Can't build graph. Redirected to text mode for debugging. Error: ${error.message}`);
try {
const editorGrammar = (yield this.editorStore.graphManagerState.graphManager.entitiesToPureCode(entities));
yield flowResult(this.editorStore.grammarTextEditorState.setGraphGrammarText(editorGrammar));
}
catch (error2) {
assertErrorThrown(error2);
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE), error2);
if (error2 instanceof NetworkClientError) {
// in case the server cannot even transform the JSON due to corrupted protocol, we can redirect to model loader
this.redirectToModelLoaderForDebugging(error2);
return {
status: GraphBuilderStatus.FAILED,
error: error2,
};
}
}
this.editorStore.setGraphEditMode(GRAPH_EDITOR_MODE.GRAMMAR_TEXT);
yield flowResult(this.globalCompileInTextMode({
ignoreBlocking: true,
suppressCompilationFailureMessage: true,
}));
return {
status: GraphBuilderStatus.REDIRECTED_TO_TEXT_MODE,
error,
};
}
return {
status: GraphBuilderStatus.FAILED,
error,
};
}
finally {
this.isInitializingGraph = false;
}
}
redirectToModelLoaderForDebugging(error) {
if (this.editorStore.isInConflictResolutionMode) {
this.editorStore.setBlockingAlert({
message: `Can't de-serialize graph model from entities`,
prompt: `Please refresh the application and abort conflict resolution`,
});
return;
}
this.editorStore.applicationStore.notifyWarning(`Can't de-serialize graph model from entities. Redirected to model loader for debugging. Error: ${error.message}`);
this.editorStore.modelLoaderState.setCurrentModelLoadType(MODEL_UPDATER_INPUT_TYPE.ENTITIES);
// Making an async call
this.editorStore.modelLoaderState.loadCurrentProjectEntities();
this.editorStore.openState(this.editorStore.modelLoaderState);
}
/**
* Get entitiy changes to prepare for syncing
*/
computeLocalEntityChanges() {
const baseHashesIndex = this.editorStore.isInConflictResolutionMode
? this.editorStore.changeDetectionState
.conflictResolutionHeadRevisionState.entityHashesIndex
: this.editorStore.changeDetectionState.workspaceLocalLatestRevisionState
.entityHashesIndex;
const originalPaths = new Set(Array.from(baseHashesIndex.keys()));
const entityChanges = [];
this.editorStore.graphManagerState.graph.allOwnElements.forEach((element) => {
const elementPath = element.path;
if (baseHashesIndex.get(elementPath) !== element.hashCode) {
const entity = this.editorStore.graphManagerState.graphManager.elementToEntity(element, {
pruneSourceInformation: true,
});
entityChanges.push({
classifierPath: entity.classifierPath,
entityPath: element.path,
content: entity.content,
type: baseHashesIndex.get(elementPath) !== undefined
? EntityChangeType.MODIFY
: EntityChangeType.CREATE,
});
}
originalPaths.delete(elementPath);
});
Array.from(originalPaths).forEach((path) => {
entityChanges.push({
type: EntityChangeType.DELETE,
entityPath: path,
});
});
return entityChanges;
}
/**
* Loads entity changes to graph and updates application.
*/
*loadEntityChangesToGraph(changes, baseEntities) {
try {
assertTrue(this.editorStore.isInFormMode, `Can't apply entity changes: operation only supported in form mode`);
const entities = baseEntities ??
this.editorStore.graphManagerState.graph.allOwnElements.map((element) => this.editorStore.graphManagerState.graphManager.elementToEntity(element));
const modifiedEntities = applyEntityChanges(entities, changes);
yield flowResult(this.updateGraphAndApplication(modifiedEntities));
}
catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.notifyError(`Can't load entity changes: ${error.message}`);
}
}
// TODO: when we support showing multiple notifications, we can take this options out as the only users of this
// is delete element flow, where we want to say `re-compiling graph after deletion`, but because compilation
// sometimes is so fast, the message flashes, so we want to combine with the message in this method
*globalCompileInFormMode(options) {
assertTrue(this.editorStore.isInFormMode, 'Editor must be in form mode to call this method');
if (this.checkIfApplicationUpdateOperationIsRunning()) {
return;
}
this.isRunningGlobalCompile = true;
try {
this.clearCompilationError();
if (options?.openConsole) {
this.editorStore.setActiveAuxPanelMode(AUX_PANEL_MODE.CONSOLE);
}
// NOTE: here we always keep the source information while compiling in form mode
// so that the form parts where the user interacted with (i.e. where the lamdbas source
// information are populated), can reveal compilation error. If compilation errors
// show up in other parts, the user will get redirected to text-mode
yield this.editorStore.graphManagerState.graphManager.compileGraph(this.editorStore.graphManagerState.graph, {
keepSourceInformation: true,
});
if (!options?.disableNotificationOnSuccess) {
this.editorStore.applicationStore.notifySuccess('Compiled successfully');
}
}
catch (error) {
assertErrorThrown(error);
// TODO: we probably should make this pattern of error the handling for all other exceptions in the codebase
// i.e. there should be a catch-all handler (we can use if-else construct to check error types)
assertType(error, EngineError, `Unhandled exception:\n${error}`);
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.COMPILATION_FAILURE), error);
let fallbackToTextModeForDebugging = true;
// if compilation failed, we try to reveal the error in form mode,
// if even this fail, we will fall back to show it in text mode
if (error instanceof CompilationError) {
const errorCoordinates = extractSourceInformationCoordinates(error.sourceInformation);
if (errorCoordinates) {
const element = this.editorStore.graphManagerState.graph.getNullableElement(guaranteeNonNullable(errorCoordinates[0], `Can't reveal compilation error: element path is missing`), false);
if (element) {
this.editorStore.openElement(element);
if (this.editorStore.currentEditorState instanceof ElementEditorState) {
// check if we can reveal the error in the element editor state
fallbackToTextModeForDebugging =
!this.editorStore.currentEditorState.revealCompilationError(error);
}
}
}
}
// decide if we need to fall back to text mode for debugging
if (fallbackToTextModeForDebugging) {
// TODO: when we support showing multiple notifications, we can split this into 2
this.editorStore.applicationStore.notifyWarning(options?.message ??
'Compilation failed and error cannot be located in form mode. Redirected to text mode for debugging.');
try {
const code = (yield this.editorStore.graphManagerState.graphManager.graphToPureCode(this.editorStore.graphManagerState.graph));
this.editorStore.grammarTextEditorState.setGraphGrammarText(code);
}
catch (error2) {
assertErrorThrown(error2);
this.editorStore.applicationStore.notifyWarning(`Can't enter text mode. Transformation to grammar text failed: ${error2.message}`);
return;
}
this.editorStore.setGraphEditMode(GRAPH_EDITOR_MODE.GRAMMAR_TEXT);
yield flowResult(this.globalCompileInTextMode({
ignoreBlocking: true,
suppressCompilationFailureMessage: true,
}));
}
else {
this.editorStore.applicationStore.notifyWarning(`Compilation failed: ${error.message}`);
}
}
finally {
this.isRunningGlobalCompile = false;
}
}
// TODO: when we support showing multiple notifications, we can take this `suppressCompilationFailureMessage` out as
// we can show the transition between form mode and text mode warning and the compilation failure warning at the same time
*globalCompileInTextMode(options) {
assertTrue(this.editorStore.isInGrammarTextMode, 'Editor must be in text mode to call this method');
if (!options?.ignoreBlocking &&
this.checkIfApplicationUpdateOperationIsRunning()) {
return;
}
try {
this.isRunningGlobalCompile = true;
this.clearCompilationError();
if (options?.openConsole) {
this.editorStore.setActiveAuxPanelMode(AUX_PANEL_MODE.CONSOLE);
}
const entities = (yield this.editorStore.graphManagerState.graphManager.compileText(this.editorStore.grammarTextEditorState.graphGrammarText, this.editorStore.graphManagerState.graph));
this.editorStore.applicationStore.notifySuccess('Compiled successfully');
yield flowResult(this.updateGraphAndApplication(entities));
}
catch (error) {
assertErrorThrown(error);
if (error instanceof EngineError) {
this.editorStore.grammarTextEditorState.setError(error);
}
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.COMPILATION_FAILURE), 'Compilation failed:', error);
if (!this.editorStore.applicationStore.notification ||
!options?.suppressCompilationFailureMessage) {
this.editorStore.applicationStore.notifyWarning(`Compilation failed: ${error.message}`);
}
}
finally {
this.isRunningGlobalCompile = false;
}
}
*leaveTextMode() {
assertTrue(this.editorStore.isInGrammarTextMode, 'Editor must be in text mode to call this method');
if (this.checkIfApplicationUpdateOperationIsRunning()) {
return;
}
try {
this.isApplicationLeavingTextMode = true;
this.clearCompilationError();
this.editorStore.setBlockingAlert({
message: 'Compiling graph before leaving text mode...',
showLoading: true,
});
try {
const entities = (yield this.editorStore.graphManagerState.graphManager.compileText(this.editorStore.grammarTextEditorState.graphGrammarText, this.editorStore.graphManagerState.graph,
// surpress the modal to reveal error properly in the text editor
// if the blocking modal is not dismissed, the edior will not be able to gain focus as modal has a focus trap
// therefore, the editor will not be able to get the focus
{ onError: () => this.editorStore.setBlockingAlert(undefined) }));
this.editorStore.setBlockingAlert({
message: 'Leaving text mode and rebuilding graph...',
showLoading: true,
});
yield flowResult(this.updateGraphAndApplication(entities));
this.editorStore.grammarTextEditorState.setGraphGrammarText('');
this.editorStore.grammarTextEditorState.resetCurrentElementLabelRegexString();
this.editorStore.setGraphEditMode(GRAPH_EDITOR_MODE.FORM);
if (this.editorStore.currentEditorState) {
this.editorStore.openState(this.editorStore.currentEditorState);
}
}
catch (error) {
assertErrorThrown(error);
if (error instanceof EngineError) {
this.editorStore.grammarTextEditorState.setError(error);
}
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.COMPILATION_FAILURE), 'Compilation failed:', error);
if (this.editorStore.graphManagerState.graphBuildState.hasFailed) {
// TODO: when we support showing multiple notification, we can split this into 2 messages
this.editorStore.applicationStore.notifyWarning(`Can't build graph, please resolve compilation error before leaving text mode. Compilation failed with error: ${error.message}`);
}
else {
this.editorStore.applicationStore.notifyWarning(`Compilation failed: ${error.message}`);
this.editorStore.setActionAlertInfo({
message: 'Project is not in a compiled state',
prompt: 'All changes made since the last time the graph was built successfully will be lost',
type: ActionAlertType.CAUTION,
onEnter: () => this.editorStore.setBlockGlobalHotkeys(true),
onClose: () => this.editorStore.setBlockGlobalHotkeys(false),
actions: [
{
label: 'Discard Changes',
handler: () => this.editorStore.setGraphEditMode(GRAPH_EDITOR_MODE.FORM),
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
},
{
label: 'Stay',
default: true,
type: ActionAlertActionType.PROCEED,
},
],
});
}
}
}
catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.COMPILATION_FAILURE), error);
}
finally {
this.isApplicationLeavingTextMode = false;
this.editorStore.setBlockingAlert(undefined);
}
}
/**
* This function is used in lambda editor in form mode when user try to do an action that involves the lambda being edited, it takes an action
* and proceeds with a parsing check for the current lambda before executing the action. This prevents case where user quickly type something
* that does not parse and hit compile or generate right away.
*/
*checkLambdaParsingError(lambdaHolderElement, checkParsingError, onSuccess) {
this.clearCompilationError();
lambdaHolderElement.clearErrors();
if (checkParsingError) {
yield flowResult(lambdaHolderElement.convertLambdaGrammarStringToObject());
// abort action if parser error occurred
if (lambdaHolderElement.parserError) {
return;
}
}
yield onSuccess();
}
/**
* NOTE: IMPORTANT! This method is both a savior and a sinner. It helps reprocessing the graph state to use a new graph
* built from the new model context data, it resets the graph properly. The bane here is that resetting the graph properly is
* not trivial, for example, in the cleanup phase, there are things we want to re-use, such as the one-time processed system
* metamodels or the `reusable` metamodels from project dependencies. There are also explorer states like the package tree,
* opened tabs, change detection, etc. to take care of. There are a lot of potential pitfalls. For these, we will add the
* marker:
*
* @risk memory-leak
*
* to indicate we should check carefully these pieces when we detect memory issue as it might still
* be referring to the old graph
*
* In the past, we have found that there are a few potential root causes for memory leak:
* 1. State management Mobx allows references, as such, it is sometimes hard to trace down which references can cause problem
* We have to understand that the behind this updater is very simple (replace), yet to do it cleanly is not easy, since
* so far it is tempting to refer to elements in the graph from various editor state. On top of that, change detection
* sometimes obfuscate the investigation but we have cleared it out with explicit disposing of reaction
* 2. Reusable models, at this point in time, we haven't completed stabilize the logic for handling generated models, as well
* as depdendencies, we intended to save computation time by reusing these while updating the graph. This can pose potential
* danger as well. Beware the way when we start to make system/project dependencies references elements of current graph
* e.g. when we have a computed value in a immutable class that get all subclasses, etc.
* 3. We reprocess editor states to ensure good UX, e.g. find tabs to keep open, find tree nodes to expand, etc.
* after updating the graph. These in our experience is the **MOST COMMON** source of memory leak. It is actually
* quite predictable since structures like tabs and tree node embeds graph data, which are references to the old graph
*
* NOTE: One big obfuscating factor is overlapping graph refresh. Sometimes, we observed that calling this update graph
* method multiple times can throws Mobx off and causes reusing change detection state to cause memory-leak. As such,
* we have blocked the possibility of calling compilation/graph-update/generation simultaneously
*
* A note on how to debug memory-leak issue:
* 1. Open browser Memory monitor
* 2. Go to text mode and compile multiple times (triggering graph update)
* 3. Try to force garbage collection, if we see memory goes up after while, it's pretty clear that this is memory-leak
* (note that since we disallow stacking multiple compilation and graph update, we have simplify the detection a lot)
* See https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/
*/
*updateGraphAndApplication(entities) {
const startTime = Date.now();
this.isUpdatingApplication = true;
this.isUpdatingGraph = true;
try {
const newGraph = this.editorStore.graphManagerState.createEmptyGraph();
/**
* NOTE: this can post memory-leak issue if we start having immutable elements referencing current graph elements:
* e.g. subclass analytics on the immutable class, etc.
*
* @risk memory-leak
*/
if (this.editorStore.graphManagerState.dependenciesBuildState.hasSucceeded) {
newGraph.dependencyManager =
this.editorStore.graphManagerState.graph.dependencyManager;
}
else {
this.editorStore.projectConfigurationEditorState.setProjectConfiguration(ProjectConfiguration.serialization.fromJson((yield this.editorStore.sdlcServerClient.getConfiguration(this.editorStore.sdlcState.activeProject.projectId, this.editorStore.sdlcState.activeWorkspace))));
const dependencyManager = this.editorStore.graphManagerState.createEmptyDependencyManager();
newGraph.dependencyManager = dependencyManager;
const dependenciesBuildState = ActionState.create();
yield this.editorStore.graphManagerState.graphManager.buildDependencies(this.editorStore.graphManagerState.coreModel, this.editorStore.graphManagerState.systemModel, dependencyManager, (yield flowResult(this.getIndexedDependencyEntities())), dependenciesBuildState);
this.editorStore.graphManagerState.dependenciesBuildState =
dependenciesBuildState;
}
/**
* Backup and editor states info before resetting
*
* @risk memory-leak
*/
const openedEditorStates = this.editorStore.openedEditorStates;
const currentEditorState = this.editorStore.currentEditorState;
/**
* We remove the current editor state so that we no longer let React displays the element that belongs to the old graph
* NOTE: this causes an UI flash, but this is in many way, acceptable since the user probably should know that we are
* refreshing the memory graph anyway.
*
* If this is really bothering, we can handle it by building mocked replica of the current editor state using stub element
* e.g. if the current editor is a class, we stub the class, create a new class editor state around it and copy over
* navigation information, etc.
*/
this.editorStore.closeAllEditorTabs();
this.editorStore.changeDetectionState.stop(); // stop change detection before disposing hash
yield flowResult(graph_dispose(this.editorStore.graphManagerState.graph));
const graphBuildState = ActionState.create();
yield this.editorStore.graphManagerState.graphManager.buildGraph(newGraph, entities, graphBuildState, {
TEMPORARY__preserveSectionIndex: this.editorStore.applicationStore.config.options
.TEMPORARY__preserveSectionIndex,
});
// Activity States
this.editorStore.globalTestRunnerState = new GlobalTestRunnerState(this.editorStore, this.editorStore.sdlcState);
// NOTE: build model generation entities every-time we rebuild the graph - should we do this?
const generationsBuildState = ActionState.create();
yield this.editorStore.graphManagerState.graphManager.buildGenerations(newGraph, this.graphGenerationState.generatedEntities, generationsBuildState);
this.editorStore.graphManagerState.graph = newGraph;
this.editorStore.graphManagerState.graphBuildState = graphBuildState;
this.editorStore.graphManagerState.generationsBuildState =
generationsBuildState;
/**
* Reprocess explorer tree which might still hold references to old graph
*
* FIXME: we allow this so the UX stays the same but this can cause memory leak
* we could consider doing this properly using node IDs
*
* @risk memory-leak
*/
this.editorStore.explorerTreeState.reprocess();
// this.editorStore.explorerTreeState = new ExplorerTreeState(this.applicationStore, this.editorStore);
// this.editorStore.explorerTreeState.buildImmutableModelTrees();
// this.editorStore.explorerTreeState.build();
/**
* Reprocess editor states which might still hold references to old graph
*
* FIXME: we allow this so the UX stays the same but this can cause memory leak
* we should change `reprocess` model to do something like having source information
* on the form to navigate to it properly so that information is not dependent on the
* graph, but on the component itself, with IDs and such.
*
* @risk memory-leak
*/
this.editorStore.openedEditorStates = openedEditorStates
.map((editorState) => this.editorStore.reprocessElementEditorState(editorState))
.filter(isNonNullable);
this.editorStore.setCurrentEditorState(this.editorStore.findCurrentEditorState(currentEditorState));
this.editorStore.applicationStore.log.info(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_UPDATED_AND_REBUILT), '[TOTAL]', Date.now() - startTime, 'ms');
this.isUpdatingGraph = false;
// ======= (RE)START CHANGE DETECTION =======
yield flowResult(this.editorStore.changeDetectionState.observeGraph());
yield this.editorStore.changeDetectionState.preComputeGraphElementHashes();
this.editorStore.changeDetectionState.start();
this.editorStore.applicationStore.log.info(LogEvent.create(CHANGE_DETECTION_EVENT.CHANGE_DETECTION_RESTARTED), '[ASYNC]');
// ======= FINISHED (RE)START CHANGE DETECTION =======
}
catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE), error);
this.editorStore.changeDetectionState.stop(true); // force stop change detection
this.isUpdatingGraph = false;
// Note: in the future this function will probably be ideal to refactor when we have different classes for each mode
// as we would handle this error differently in `text` mode and `form` mode.
if (error instanceof GraphBuilderError && this.editorStore.isInFormMode) {
this.editorStore.applicationStore.setBlockingAlert({
message: `Can't build graph: ${error.message}`,
prompt: 'Refreshing full application...',
showLoading: true,
});
this.editorStore.closeAllEditorTabs();
this.editorStore.cleanUp();
yield flowResult(this.editorStore.buildGraph(entities));
}
else {
this.editorStore.applicationStore.notifyError(`Can't build graph: ${error.message}`);
}
}
finally {
this.isUpdatingApplication = false;
this.editorStore.applicationStore.setBlockingAlert(undefined);
}
}
/**
* Used to update generation model and generation graph using the generated entities
* does not alter the main or dependency model
*/
*updateGenerationGraphAndApplication() {
assertTrue(this.editorStore.graphManagerState.graphBuildState.hasSucceeded &&
this.editorStore.graphManagerState.dependenciesBuildState.hasSucceeded, 'Both main model and dependencies must be processed to built generation graph');
this.isUpdatingApplication = true;
try {
/**
* Backup and editor states info before resetting
*
* @risk memory-leak
*/
const openedEditorStates = this.editorStore.openedEditorStates;
const currentEditorState = this.editorStore.currentEditorState;
this.editorStore.closeAllEditorTabs();
yield flowResult(this.editorStore.graphManagerState.graph.generationModel.dispose());
// we reset the generation model
this.editorStore.graphManagerState.graph.generationModel =
this.editorStore.graphManagerState.createEmptyGenerationModel();
yield this.editorStore.graphManagerState.graphManager.buildGenerations(this.editorStore.graphManagerState.graph, this.graphGenerationState.generatedEntities, this.editorStore.graphManagerState.generationsBuildState);
/**
* Reprocess explorer tree which might still hold references to old graph
*
* FIXME: we allow this so the UX stays the same but this can cause memory leak
* we could consider doing this properly using node IDs
*
* @risk memory-leak
*/
this.editorStore.explorerTreeState.reprocess();
// so that information is not dependent on the graph, but on the component itself, with IDs and such.
this.editorStore.openedEditorStates = openedEditorStates
.map((editorState) => this.editorStore.reprocessElementEditorState(editorState))
.filter(isNonNullable);
this.editorStore.setCurrentEditorState(this.editorStore.findCurrentEditorState(currentEditorState));
}
catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE), error);
this.editorStore.applicationStore.notifyError(`Can't build graph: ${error.message}`);
}
finally {
this.isUpdatingApplication = false;
}
}
*getIndexedDependencyEntities() {
const dependencyEntitiesIndex = new Map();
const currentConfiguration = this.editorStore.projectConfigurationEditorState
.currentProjectConfiguration;
try {
if (currentConfiguration.projectDependencies.length) {
const dependencyCoordinates = (yield flowResult(this.buildProjectDependencyCoordinates(currentConfiguration.projectDependencies)));
// NOTE: if A@v1 is transitive dependencies of 2 or more
// direct dependencies, metadata server will take care of deduplication
const dependencyEntitiesJson = (yield this.editorStore.depotServerClient.collectDependencyEntities(dependencyCoordinates.map((e) => ProjectDependencyCoordinates.serialization.toJson(e)), true, true));
const dependencyEntities = dependencyEntitiesJson.map((e) => ProjectVersionEntities.serialization.fromJson(e));
const dependencyProjects = new Set();
dependencyEntities.forEach((dependencyInfo) => {
const projectId = dependencyInfo.id;
// There are a few validations that must be done:
// 1. Unlike above, if in the depdendency graph, we have both A@v1 and A@v2
// then we need to throw. Both SDLC and metadata server should handle this
// validation, but haven't, so for now, we can do that in Studio.
// 2. Same as the previous case, but for version-to-version transformation
// This is a special case that needs handling, right now, SDLC does auto
// healing, by scanning all the path and convert them into versioned path
// e.g. model::someClass -> project1::v1_0_0::model::someClass
// But this is a rare and advanced use-case which we will not attempt to handle now.
if (dependencyProjects.has(projectId)) {
const projectVersions = dependencyEntities
.filter((e) => e.id === projectId)
.map((e) => e.versionId);
throw new UnsupportedOperationError(`Depending on multiple versions of a project is not supported. Found dependency on project '${projectId}' with versions: ${projectVersions.join(', ')}.`);
}
dependencyEntitiesIndex.set(dependencyInfo.id, dependencyInfo.entities);
dependencyProjects.add(dependencyInfo.id);
});
}
}
catch (error) {
assertErrorThrown(error);
const message = `Can't acquire dependency entitites. Error: ${error.message}`;
this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE), message);
this.editorStore.applicationStore.notifyError(error);
throw new DependencyGraphBuilderError(error);
}
return dependencyEntitiesIndex;
}
*buildProjectDependencyCoordinates(projectDependencies) {
return (yield Promise.all(projectDependencies.map((dep) => {
// legacyDependencies
// We do this for backward compatible reasons as we expect current dependency ids to be in the format of {groupId}:{artifactId}.
// For the legacy dependency we must fetch the corresponding coordinates (group, artifact ids) from the depot server
if (dep.isLegacyDependency) {
return this.editorStore.depotServerClient
.getProjectById(dep.projectId)
.then((projects) => {
const projectsData = projects.map((p) => ProjectData.serialization.fromJson(p));
if (projectsData.length !== 1) {
throw new Error(`Expected 1 project for project ID '${dep.projectId}'. Got ${projectsData.length} projects with coordinates ${projectsData
.map((i) => `'${generateGAVCoordinates(i.groupId, i.artifactId, undefined)}'`)
.join(', ')}.`);
}
const project = projectsData[0];
return new ProjectDependencyCoordinates(project.groupId, project.artifactId, dep.versionId);
});
}
else {
return Promise.resolve(new ProjectDependencyCoordinates(guaranteeNonNullable(dep.groupId), guaranteeNonNullable(dep.artifactId), dep.versionId));
}
})));
}
// -------------------------------------------------- UTILITIES -----------------------------------------------------
/**
* NOTE: Notice how this utility draws resources from all of metamodels and uses `instanceof` to classify behavior/response.
* As such, methods in this utility cannot be placed in place they should belong to.
*
* For example: `getSetImplemetnationType` cannot be placed in `SetImplementation` because of circular module dependency
* So this utility is born for such purpose, to avoid circular module dependency, and it should just be used for only that
* Other utilities that really should reside in the domain-specific meta model should be placed in the meta model module.
*
* NOTE: We expect the need for these methods will eventually go away as we complete modularization. But we need these
* methods here so that we can load plugins.
*/
getPackageableElementType(element) {
if (element instanceof PrimitiveType) {
return PACKAGEABLE_ELEMENT_TYPE.PRIMITIVE;
}
else if (element instanceof Package) {
return PACKAGEABLE_ELEMENT_TYPE.PACKAGE;
}
else if (element instanceof Class) {
return PACKAGEABLE_ELEMENT_TYPE.CLASS;
}
else if (element instanceof Association) {
return PACKAGEABLE_ELEMENT_TYPE.ASSOCIATION;
}
else if (element instanceof Enumeration) {
return PACKAGEABLE_ELEMENT_TYPE.ENUMERATION;
}
else if (element instanceof Measure) {
return PACKAGEABLE_ELEMENT_TYPE.MEASURE;
}
else if (element instanceof Unit) {
return PACKAGEABLE_ELEMENT_TYPE.UNIT;
}
else if (element instanceof Profile) {
return PACKAGEABLE_ELEMENT_TYPE.PROFILE;
}
else if (element instanceof ConcreteFunctionDefinition) {
return PACKAGEABLE_ELEMENT_TYPE.FUNCTION;
}
else if (element instanceof FlatData) {
return PACKAGEABLE_ELEMENT_TYPE.FLAT_DATA_STORE;
}
else if (element instanceof Database) {
return PACKAGEABLE_ELEMENT_TYPE.DATABASE;
}
else if (element instanceof Mapping) {
return PACKAGEABLE_ELEMENT_TYPE.MAPPING;
}
else if (element instanceof Service) {
return PACKAGEABLE_ELEMENT_TYPE.SERVICE;
}
else if (element instanceof PackageableConnection) {
return PACKAGEABLE_ELEMENT_TYPE.CONNECTION;
}
else if (element instanceof PackageableRuntime) {
return PACKAGEABLE_ELEMENT_TYPE.RUNTIME;
}
else if (element instanceof FileGenerationSpecification) {
return PACKAGEABLE_ELEMENT_TYPE.FILE_GENERATION;
}
else if (element instanceof GenerationSpecification) {
return PACKAGEABLE_ELEMENT_TYPE.GENERATION_SPECIFICATION;
}
else if (element instanceof SectionIndex) {
return PACKAGEABLE_ELEMENT_TYPE.SECTION_INDEX;
}
else if (element instanceof DataElement) {
return PACKAGEABLE_ELEMENT_TYPE.DATA;
}
const extraElementTypeLabelGetters = this.editorStore.pluginManager
.getApplicationPlugins()
.flatMap((plugin) => plugin.getExtraElementTypeGetters?.() ?? []);
for (const labelGetter of extraElementTypeLabelGetters) {
const label = labelGetter(element);
if (label) {
return label;
}
}
throw new UnsupportedOperationError(`Can't get type label for element '${element.path}': no compatible label getter available from plugins`);
}
getSetImplementationType(setImplementation) {
if (setImplementation instanceof PureInstanceSetImplementation) {
return SET_IMPLEMENTATION_TYPE.PUREINSTANCE;
}
else if (setImplementation instanceof OperationSetImplementation) {
return SET_IMPLEMENTATION_TYPE.OPERATION;
}
else if (setImplementation instanceof FlatDataInstanceSetImplementation) {
return SET_IMPLEMENTATION_TYPE.FLAT_DATA;
}
else if (setImplementation instanceof EmbeddedFlatDataPropertyMapping) {
return SET_IMPLEMENTATION_TYPE.EMBEDDED_FLAT_DATA;
}
else if (setImplementation instanceof RootRelationalInstanceSetImplementation) {
return SET_IMPLEMENTATION_TYPE.RELATIONAL;
}
else if (setImplementation instanceof EmbeddedRelationalInstanceSetImplementation) {
return SET_IMPLEMENTATION_TYPE.EMBEDDED_RELATIONAL;
}
else if (setImplementation instanceof AggregationAwareSetImplementation) {
return SET_IMPLEMENTATION_TYPE.AGGREGATION_AWARE;
}
const extraSetImplementationClassifiers = this.editorStore.pluginManager
.getApplicationPlugins()
.flatMap((plugin) => plugin.getExtraSetImp