UNPKG

@eclipse-emfcloud/trigger-engine

Version:

Generic model triggers computation engine.

168 lines 8.09 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.TriggerEngineImpl = void 0; const fast_json_patch_1 = require("fast-json-patch"); const lodash_1 = require("lodash"); /** * A default implementation of the `TriggerEngine`. */ class TriggerEngineImpl { constructor(options) { this._triggers = new Array(); this._safePatches = options?.safePatches ?? false; this._iterationLimit = options?.iterationLimit ?? 1000; this.applyTriggers = options?.parallel === true ? this.applyTriggersInParallel : this.applyTriggersInStrictSequence; if (this._iterationLimit < 50) { throw new Error(`Iteration limit too low: ${this._iterationLimit} < 50`); } } addTrigger(trigger) { this._triggers.push(trigger); } async applyTriggersInStrictSequence(document, delta, previousDocument) { // The working copy is continually updated by patches from triggers const workingCopy = this.createWorkingCopy(document); // Every trigger invocation gets the "before image" which is the // state the document was in before it evolved into the "working copy" // via the patches describing those changes. And the deltas that it // gets start with the patch that evolved the "before image" into the // initial "working copy" state (as seeded from the "document") const initialBeforeImage = this.createWorkingCopy(previousDocument); const initialDelta = delta; const triggerIterations = new Map(); let i = 0; let rollingBeforeImage = this.createWorkingCopy(document); let deltaBasis = initialBeforeImage; let workingCopyGeneration = 0; let deltaGeneration = workingCopyGeneration; let rollingDelta = initialDelta; for (; i < this._iterationLimit; i++) { // Track whether this iteration over the triggers changes anything let changedInThisIteration = false; for (const trigger of this._triggers) { const beforeImage = triggerIterations.get(trigger) ?? initialBeforeImage; let triggerDelta; // Don't recompute the delta if we can reuse the current if (deltaBasis === beforeImage && deltaGeneration === workingCopyGeneration) { triggerDelta = rollingDelta; } else { triggerDelta = this.compare(beforeImage, workingCopy); deltaBasis = beforeImage; deltaGeneration = workingCopyGeneration; rollingDelta = triggerDelta; } // If there were no delta, then there would have been not changes in // the previous iteration and we would have stopped before now. const triggerPatch = await trigger(workingCopy, triggerDelta, beforeImage); if (triggerPatch && triggerPatch.length) { changedInThisIteration = true; // Capture the current state in the rolling "before image" rollingBeforeImage = this.createWorkingCopy(workingCopy); // Update the working copy for the next trigger this.applyPatch(workingCopy, triggerPatch); workingCopyGeneration++; } // Record the trigger's "before image" for the next iteration triggerIterations.set(trigger, rollingBeforeImage); } if (!changedInThisIteration) { // Done with iterating triggers as this iteration through them produced // no new changes break; } } if (i >= this._iterationLimit) { throw new Error(`Trigger iteration limit of ${this._iterationLimit} exceeded`); } // Compute an optimized delta, considering that maybe the // sum of all the patches is zero const result = i > 0 ? this.compare(document, workingCopy) : []; return result.length ? result : undefined; } async applyTriggersInParallel(document, delta, previousDocument) { const workingCopy = this.createWorkingCopy(document); let beforeImage = this.createWorkingCopy(previousDocument); let patched = false; let workingCopyDelta = delta; let i = 0; for (; i < this._iterationLimit; i++) { const nextPatch = []; for (const trigger of this._triggers) { const triggerPatch = await trigger(workingCopy, workingCopyDelta, beforeImage); if (triggerPatch && triggerPatch.length) { nextPatch.push(...triggerPatch); } } if (nextPatch.length === 0) { break; } patched = true; // Retain the previous state of the working copy for the next iteration beforeImage = this.createWorkingCopy(workingCopy); // Update the working copy for the next round of iteration this.applyPatch(workingCopy, nextPatch); // Recompute the delta to normalize the patch and eliminate '-' pseudoindices workingCopyDelta = this.compare(beforeImage, workingCopy); } if (i >= this._iterationLimit) { throw new Error(`Trigger iteration limit of ${this._iterationLimit} exceeded`); } // Compute an optimized delta, considering that maybe the // sum of all the patches is zero const result = patched ? this.compare(document, workingCopy) : []; return result.length ? result : undefined; } /** * Create a working copy of the given `document` that it is safe to * modify by patching, to compute further patches from triggers. * * @param document the document to be patched * @returns a safely patchable working copy of the `document` */ createWorkingCopy(document) { return (0, lodash_1.cloneDeep)(document); } /** * Apply a patch to the working copy of the document under computation. * * @param workingCopy the working copy of the document on which triggers are being computed * @param patch a patch to apply to the working copy */ applyPatch(workingCopy, patch) { const patchToApply = this._safePatches ? (0, lodash_1.cloneDeep)(patch) : patch; (0, fast_json_patch_1.applyPatch)(workingCopy, patchToApply, true, true); } /** * Compare a `document` with the `workingCopy` evolved from it to produce a patch * describing the total changes to be applied to the `document` to effect the * totality of triggered follow-ups changes. * * @param document the original document for which triggers are computed * @param workingCopy the working copy resulting from iterative application of triggers * @returns the delta between the `document` as pre-image and the `workingCopy` as post-image */ compare(document, workingCopy) { return (0, fast_json_patch_1.compare)(document, workingCopy, true); } } exports.TriggerEngineImpl = TriggerEngineImpl; //# sourceMappingURL=trigger-engine.js.map