@eclipse-emfcloud/model-manager
Version:
Command-based model editing with undo/redo.
132 lines (120 loc) • 4.53 kB
text/typescript
// *****************************************************************************
// 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
// *****************************************************************************
import { EditingContext } from '../core';
import { getLogger } from '@eclipse-emfcloud/model-logger';
const logger = getLogger('model-manager/promise-util');
/**
* Utility for execution of async functions that must be given mutually
* exclusive access to some resource. Execution is chained: each async
* call that is passed in for execution is deferred until the previous
* has completed, whether normally or with an error.
*/
export class ExclusiveExecutor<K> {
/**
* Awaitable results of the current pending exclusive operations.
* Initially there's nothing pending.
*/
private locks: Lock<K>[] = [];
/**
* Exclusively run the given asynchronous `operation`, returning its
* eventual result. Operations can be executed in parallel only if they
* affect different models and editing contexts. If modelIds is not
* specified, the operation is assumed to potentially affect any model,
* and will not run in parallel with any other operation.
*
* @param operation an asynchronous operation to run exclusively
* @param contexts the editing contexts affected by the operation
* @param modelIds the identifier of models modified by the operation
* @returns the eventual result of the `operation`
*/
run<T>(
operation: () => Promise<T>,
contexts: EditingContext[],
modelIds?: K[]
): Promise<T> {
const dependentLock = this.getDependentLock(contexts, modelIds);
const result = dependentLock.pendingExclusiveOperation.then(() =>
operation()
);
// Don't propagate any error to the next caller in the chain
const lockOperation = new Promise<void>((resolve) => {
result
.catch((error) => {
if (error instanceof Error) {
logger.debug(
'Error occurred during command execution:',
error.message
);
} else {
logger.debug('Error occurred during command execution.', error);
}
})
.finally(resolve);
});
const lock: Lock<K> = {
modelIds,
contexts,
pendingExclusiveOperation: lockOperation,
};
this.locks.push(lock);
// Remove the lock after execution
lock.pendingExclusiveOperation.finally(() => {
const lockIndex = this.locks.indexOf(lock);
this.locks.splice(lockIndex, 1);
});
return result;
}
private getDependentLock(
contexts: EditingContext[],
modelIds?: K[]
): Lock<K> {
const waitForLocks: Promise<unknown>[] = [];
if (modelIds === undefined) {
// If the model ids are missing, lock all models. This is typically
// the case for undo/redo operations, which we shouldn't be running
// in parallel.
waitForLocks.push(
...this.locks.map((lock) => lock.pendingExclusiveOperation)
);
} else {
for (const lock of this.locks) {
// If the editing contexts overlap, we need to wait for the previous operation.
if (lock.contexts.some((context) => contexts.includes(context))) {
waitForLocks.push(lock.pendingExclusiveOperation);
} else {
if (lock.modelIds === undefined) {
// Wait for all operations that lock all models.
waitForLocks.push(lock.pendingExclusiveOperation);
} else {
// If the model ids are present, only wait for overlapping locks.
if (lock.modelIds.some((modelId) => modelIds.includes(modelId))) {
waitForLocks.push(lock.pendingExclusiveOperation);
}
}
}
}
}
return {
modelIds,
contexts,
pendingExclusiveOperation: Promise.allSettled(waitForLocks),
};
}
}
type Lock<K> = {
modelIds?: K[];
contexts: EditingContext[];
pendingExclusiveOperation: Promise<unknown>;
};