@eclipse-emfcloud/model-manager
Version:
Command-based model editing with undo/redo.
157 lines • 6.59 kB
JavaScript
;
// *****************************************************************************
// 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
// *****************************************************************************
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = __importStar(require("chai"));
const chai_as_promised_1 = __importDefault(require("chai-as-promised"));
const fast_json_patch_1 = require("fast-json-patch");
const lodash_1 = require("lodash");
const __1 = require("..");
const core_1 = require("../../core");
chai_1.default.use(chai_as_promised_1.default);
const _modelA = {
aString: 'A string',
aNumber: 42,
details: [{ stringDetail: 'detail', numericDetail: 7 }],
};
const MODELA_ID = 'test:modelA';
describe('Concurrent model modifications', () => {
const getModelA = () => {
const modelA = manager.getModel(MODELA_ID);
(0, chai_1.expect)(modelA).to.exist;
return modelA;
};
let manager;
let stack;
beforeEach(() => {
manager = new __1.CoreModelManagerImpl();
stack = manager.getCommandStack();
manager.setModel(MODELA_ID, (0, lodash_1.cloneDeep)(_modelA));
});
it('sequential command execution', async () => {
await stack.execute(new NumberCommand(MODELA_ID, (aNumber) => aNumber + 2), MODELA_ID);
await stack.execute(new NumberCommand(MODELA_ID, (aNumber) => aNumber / 11), MODELA_ID);
(0, chai_1.expect)(getModelA().aNumber).to.be.equal(4);
});
it('concurrent command execution', async () => {
const first = stack.execute(new NumberCommand(MODELA_ID, (aNumber) => aNumber + 2), MODELA_ID);
const second = stack.execute(new NumberCommand(MODELA_ID, (aNumber) => aNumber / 11), MODELA_ID);
await (0, chai_1.expect)(first).to.eventually.be.fulfilled;
await (0, chai_1.expect)(second).to.eventually.be.fulfilled;
(0, chai_1.expect)(getModelA().aNumber).to.be.equal(4);
});
it('permits no dirty reads', async () => {
const first = new NumberCommand(MODELA_ID, (aNumber) => aNumber + 2);
const second = new NumberCommand(MODELA_ID, (aNumber) => aNumber / 11);
const composite = (0, core_1.append)(first, second);
const execution = stack.execute(composite, MODELA_ID);
const attemptedDirtyRead = first.executed.then(() => getModelA().aNumber);
await (0, chai_1.expect)(attemptedDirtyRead).to.eventually.be.equal(42);
await (0, chai_1.expect)(execution).to.eventually.be.fulfilled;
(0, chai_1.expect)(getModelA().aNumber).to.be.equal(4);
});
it('copies models on write', async () => {
const initialModelA = getModelA();
const first = new NumberCommand(MODELA_ID, (aNumber) => aNumber + 2);
const second = new NumberCommand(MODELA_ID, (aNumber) => aNumber / 11);
const composite = (0, core_1.append)(first, second);
const execution = stack.execute(composite, MODELA_ID);
const intermediateModelA = await first.executed.then(getModelA);
await execution;
const finalModelA = getModelA();
(0, chai_1.expect)(intermediateModelA).to.be.equal(initialModelA);
(0, chai_1.expect)(finalModelA).not.to.be.equal(initialModelA);
});
});
async function pause() {
return new Promise((resolve) => {
setImmediate(resolve);
});
}
class NumberCommand {
constructor(modelId, computation) {
this.modelId = modelId;
this.computation = computation;
this.executed = new Promise((resolve) => {
this.resolveExecuted = resolve;
});
}
async execute(model) {
const modelA = model;
const newNumber = this.computation(modelA.aNumber);
// Yield execution to some other concurrent promise
await pause();
const patch = [
{ op: 'replace', path: '/aNumber', value: newNumber },
];
const original = (0, lodash_1.cloneDeep)(modelA);
(0, fast_json_patch_1.applyPatch)(modelA, patch);
this.patch = (0, fast_json_patch_1.compare)(modelA, original, true);
this.resolveExecuted();
return patch;
}
applyAndReversePatch(model) {
const original = (0, lodash_1.cloneDeep)(model);
if (!this.patch) {
throw new Error('Not yet executed');
}
const patch = this.patch;
(0, fast_json_patch_1.applyPatch)(model, patch);
this.patch = (0, fast_json_patch_1.compare)(model, original, true);
return patch;
}
undo(model) {
return this.applyAndReversePatch(model);
}
redo(model) {
return this.applyAndReversePatch(model);
}
canExecute() {
return !this.patch;
}
canUndo() {
return !!this.patch;
}
canRedo() {
return !!this.patch;
}
}
//# sourceMappingURL=concurrency.spec.js.map