UNPKG

@eclipse-emfcloud/model-service

Version:
370 lines 16.1 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2023-2024 STMicroelectronics. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: MIT License which is // available at https://opensource.org/licenses/MIT. // // SPDX-License-Identifier: EPL-2.0 OR MIT // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.ModelHubImpl = void 0; const model_logger_1 = require("@eclipse-emfcloud/model-logger"); const model_validation_1 = require("@eclipse-emfcloud/model-validation"); const lodash_1 = require("lodash"); const model_trigger_engine_1 = require("./model-trigger-engine"); const logger = (0, model_logger_1.getLogger)('model-service/model-hub-impl'); class ModelHubImpl { constructor(context, modelManager, validationService, modelAccessorBus) { this.context = context; this.modelManager = modelManager; this.validationService = validationService; this.modelAccessorBus = modelAccessorBus; this.contributions = new Map(); this.subscriptions = []; // The core command stack tracks dirty state per editing context, but we track // it per model ID, so we have to aggregate each editing context's state per model ID. // Key is model ID; value is editing contexts (command stack IDs) in which it is dirty this.dirtyState = new Map(); this.pendingLoads = new Map(); this.disposed = false; this.selfSubscription = this.subscribe(); this.selfSubscription.onModelLoaded = this.handleModelLoaded.bind(this); this.liveValidation = true; this.dirtyStateSub = modelManager .getCommandStack('') .getCoreCommandStack() .subscribe(); this.dirtyStateSub.onDirtyStateChanged = this.handleDirtyStateChanged.bind(this); this.modelManagerSubscription = modelManager.subscribe(); this.modelManagerSubscription.onModelLoaded = this.notifyModelLoaded.bind(this); this.modelManagerSubscription.onModelUnloaded = this.notifyModelUnloaded.bind(this); } dispose() { this.disposed = true; this.liveValidation = false; this.selfSubscription.close(); this.modelManagerSubscription.close(); Array.from(this.contributions.values()) .filter(isDisposable) .forEach((contrib) => safeCallback(contrib, contrib.dispose)); this.notifyDisposed(); this.contributions.clear(); this.subscriptions.forEach((sub) => sub.subscription.close()); this.subscriptions.length = 0; } get isDisposed() { return this.disposed; } addModelServiceContribution(modelServiceContribution) { const id = modelServiceContribution.id; if (this.contributions.has(id)) { throw new Error('A ModelServiceContribution is already registered for id: ' + id); } this.contributions.set(id, modelServiceContribution); modelServiceContribution.setModelManager(this.modelManager); modelServiceContribution.setModelAccessorBus(this.modelAccessorBus); const validators = modelServiceContribution.validationContribution?.getValidators() ?? []; validators.forEach((v) => this.validationService.addValidator(v)); modelServiceContribution.setValidationService(this.validationService); const triggers = modelServiceContribution.triggerContribution?.getTriggers() ?? []; triggers.forEach((t) => this.triggerEngine?.addModelTrigger(t)); const providers = modelServiceContribution.modelAccessorContribution?.getProviders() ?? []; providers.forEach((provider) => { if (typeof provider.setModelHub === 'function') { provider.setModelHub(this); } this.modelAccessorBus.register(provider); }); // Do not set the contribution's hub yet as this is required to be done only // after all contributions are added, configured, and available to one other } get liveValidation() { return this.liveValidationSubscription !== undefined; } set liveValidation(liveValidation) { if (liveValidation === this.liveValidation) { return; } // We're toggling live validation state if (this.liveValidationSubscription) { // Turn off live validation: close the subscription this.liveValidationSubscription.close(); this.liveValidationSubscription = undefined; } else { // Turn on live validation: create the subscription this.liveValidationSubscription = this.modelManager.subscribe(); this.liveValidationSubscription.onModelChanged = (modelId) => this.validateModels(modelId); } } get triggerEngine() { return 'triggerEngine' in this.modelManager && this.modelManager.triggerEngine instanceof model_trigger_engine_1.ModelTriggerEngine ? this.modelManager.triggerEngine : undefined; } getModelService(id) { const contribution = this.contributions.get(id); return contribution?.getModelService(); } getModelAccessorBus() { return this.modelAccessorBus; } async getModel(modelId) { // Check if model is already loaded const existingModel = this.modelManager.getModel(modelId); if (existingModel !== undefined) { return existingModel; } return this.loadModel(modelId); } // Find the persistence contribution responsible for loading // the requested model and load it. If a load of the same // model is already pending, return that pending promise. loadModel(modelId) { let result = this.pendingLoads.get(modelId); if (!result) { result = this.getPersistenceContribution(modelId).then((contrib) => contrib.loadModel(modelId)); this.pendingLoads.set(modelId, result); result .then((loadedModel) => { this.modelManager.setModel(modelId, loadedModel); }) .finally(() => this.pendingLoads.delete(modelId)); } return result; } subscribe(...modelIds) { const subscription = {}; const onModelChanged = (modelId, model, delta) => subscription.onModelChanged?.(modelId, model, delta); const onValidationChanged = (modelId, model, diagnostic) => subscription.onModelValidated?.(modelId, model, diagnostic); const entry = { modelIds: modelIds.length ? new Set(modelIds) : undefined, subscription, }; this.subscriptions.push(entry); if (modelIds.length === 0) { // Easy case: universal subscription const modelSubscription = this.modelManager.subscribe(); modelSubscription.onModelChanged = onModelChanged; const validationSubscription = this.validationService.subscribe(); validationSubscription.onValidationChanged = onValidationChanged; subscription.close = () => { modelSubscription.close(); validationSubscription.close(); (0, lodash_1.pull)(this.subscriptions, entry); }; } else { // Subscription to particular models const modelSubs = modelIds.map((id) => this.modelManager.subscribe(id)); modelSubs.forEach((sub) => (sub.onModelChanged = onModelChanged)); const validationSubs = modelIds.map((id) => this.validationService.subscribe(id)); validationSubs.forEach((sub) => (sub.onValidationChanged = onValidationChanged)); subscription.close = () => { modelSubs.forEach((sub) => sub.close()); validationSubs.forEach((sub) => sub.close()); (0, lodash_1.pull)(this.subscriptions, entry); }; } return subscription; } async save(...commandStackIds) { let result = false; const allCommandStackIds = commandStackIds.length ? commandStackIds : this.modelManager.getCommandStackIds(); for (const commandStackId of allCommandStackIds) { const commandStack = this.getCommandStack(commandStackId); const modelIdsToSave = commandStack.getDirtyModelIds(); for (const modelId of modelIdsToSave) { const persistenceContribution = await this.getPersistenceContribution(modelId); const model = await this.getModel(modelId); const saved = await persistenceContribution.saveModel(modelId, model); result ||= saved; } commandStack.markSaved(); } return result; } async validateModels(...modelIds) { const allDiagnostics = []; const allIds = modelIds.length ? modelIds : this.modelManager.getModelIds(); for (const modelId of allIds) { allDiagnostics.push(this.getModel(modelId) .then((model) => this.validationService.validate(modelId, model)) .catch((error) => ({ severity: 'error', source: '@eclipse-emfcloud/model-service', path: '', message: `Failed to load model '${modelId}'.`, data: error, }))); } return (0, model_validation_1.merge)(...(await Promise.all(allDiagnostics))); } getValidationState(...modelIds) { const allDiagnostics = []; const allIds = modelIds.length ? modelIds : this.modelManager.getModelIds(); for (const modelId of allIds) { const modelDiagnostic = this.validationService.getValidationState(modelId); if (modelDiagnostic) { allDiagnostics.push(modelDiagnostic); } } return allDiagnostics.length ? (0, model_validation_1.merge)(...allDiagnostics) : undefined; } getCommandStack(commandStackId) { return this.modelManager.getCommandStack(commandStackId); } async undo(commandStackId) { const commandStack = this.getCommandStack(commandStackId); if (await commandStack.canUndo()) { const deltas = await commandStack.undo(); return (deltas?.size ?? 0) > 0; } return false; } async redo(commandStackId) { const commandStack = this.getCommandStack(commandStackId); if (await commandStack.canRedo()) { const deltas = await commandStack.redo(); return (deltas?.size ?? 0) > 0; } return false; } flush(commandStackId) { const commandStack = this.getCommandStack(commandStackId); return commandStack.flush().length > 0; } isDirty(commandStackId) { const commandStack = this.getCommandStack(commandStackId); return commandStack.isDirty(); } handleDirtyStateChanged(...args) { const editingContext = args[0]; const modelDirtyState = args[1]; // Function to snapshot the currently dirty model IDs const computeDirtyModelIds = () => new Set(Array.from(this.dirtyState.entries()) .filter(([_, dirtyIn]) => dirtyIn.size > 0) .map(([modelId]) => modelId)); const oldDirtyModelIds = computeDirtyModelIds(); // Update my aggregate per-model dirty state for (const [modelId, isDirty] of modelDirtyState.entries()) { const dirtyState = this.dirtyState.get(modelId) ?? new Set(); this.dirtyState.set(modelId, dirtyState); if (isDirty) { dirtyState.add(editingContext); } else { dirtyState.delete(editingContext); } } const newDirtyModelIds = computeDirtyModelIds(); // Compute the symmetric difference Array.from(oldDirtyModelIds).forEach((modelId) => { if (newDirtyModelIds.delete(modelId)) { oldDirtyModelIds.delete(modelId); } }); const notify = async (isDirty, modelIds) => { modelIds.forEach((modelId) => { this.subscriptions .filter((sub) => !sub.modelIds || sub.modelIds.has(modelId)) .forEach(async (sub) => { const model = await this.getModel(modelId); if (sub.subscription.onModelDirtyState && model) { safeCallback(undefined, sub.subscription.onModelDirtyState, modelId, model, isDirty); } }); }); }; notify(false, oldDirtyModelIds); notify(true, newDirtyModelIds); } async getPersistenceContribution(modelId) { // Find a contribution able to handle the requested model for (const contribution of this.contributions.values()) { if (await contribution.persistenceContribution.canHandle(modelId)) { const result = contribution.persistenceContribution; // This cast makes sense because the contribution averred that it can handle the model return result; } } throw new Error('Failed to find a model persistence contribution for ModelID: ' + modelId); } /** * Handle the self-subscription notification of the loading of the * identified model. This default implementation validates the newly * loaded model if {@link liveValidation} is on. */ handleModelLoaded(modelId) { if (this.liveValidation) { this.validateModels(modelId); } } /** * Invoke the model-loaded call-back of subscribers for the given * model ID that have the call-back. */ notifyModelLoaded(modelId) { this.subscriptions .filter((sub) => !sub.modelIds || sub.modelIds.has(modelId)) .forEach((sub) => { if (sub.subscription.onModelLoaded) { safeCallback(undefined, sub.subscription.onModelLoaded, modelId); } }); } /** * Invoke the model-unloaded call-back of subscribers for the given * model ID that have the call-back. */ notifyModelUnloaded(modelId, model) { this.subscriptions .filter((sub) => !sub.modelIds || sub.modelIds.has(modelId)) .forEach((sub) => { if (sub.subscription.onModelUnloaded) { safeCallback(undefined, sub.subscription.onModelUnloaded, modelId, model); } }); } /** * Invoke the disposed call-back of subscribers that have the call-back. */ notifyDisposed() { this.subscriptions.forEach((sub) => { if (sub.subscription.onModelHubDisposed) { safeCallback(undefined, sub.subscription.onModelHubDisposed); } }); } } exports.ModelHubImpl = ModelHubImpl; /** * Safely invoke a call-back, reporting any uncaught exception that it * may throw, to ensure that subsequent subscriptions don't miss out. */ const safeCallback = (thisArg, callback, ...args) => { try { callback.call(thisArg, ...args); } catch (error) { logger.error('Uncaught exception in ModelHub call-back.', error); } }; /** Type predicate for a disposable object. */ const isDisposable = (target) => { return 'dispose' in target && typeof target.dispose === 'function'; }; //# sourceMappingURL=model-hub-impl.js.map