@eclipse-emfcloud/model-service
Version:
Model service framework
370 lines • 16.1 kB
JavaScript
"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