UNPKG

@finos/legend-studio

Version:
838 lines 50.5 kB
/** * 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