UNPKG

@eclipse-emfcloud/model-manager

Version:

Command-based model editing with undo/redo.

157 lines 6.59 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 // ***************************************************************************** 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