@itwin/core-backend
Version:
iTwin.js backend components
862 lines (853 loc) • 37.4 kB
JavaScript
import { BeEvent, DbResult, IModelStatus, StopWatch } from "@itwin/core-bentley";
import { Code, GeometryStreamBuilder, IModel, IModelError, RelatedElement } from "@itwin/core-common";
import { LineSegment3d, Point3d, YawPitchRollAngles } from "@itwin/core-geometry";
import * as chai from "chai";
import * as chaiAsPromised from "chai-as-promised";
import { SpatialCategory } from "../Category";
import { ChannelControl } from "../ChannelControl";
import { ClassRegistry } from "../ClassRegistry";
import { GeometricElement3d, PhysicalPartition } from "../Element";
import { IModelDb } from "../IModelDb";
import { HubMock } from "../internal/HubMock";
import { PhysicalModel } from "../Model";
import { SubjectOwnsPartitionElements } from "../NavigationRelationship";
import { ElementDrivesElement } from "../Relationship";
import { Schema, Schemas } from "../Schema";
import { HubWrappers } from "./IModelTestUtils";
import { KnownTestLocations } from "./KnownTestLocations";
chai.use(chaiAsPromised);
/**
1. What is Change Propagation?**
In engineering, models often consist of many interdependent components (e.g., parts, assemblies, constraints). When you modify one component (say, changing a dimension), that change can affect other components.
**Change propagation** is the process of updating all dependent components so the design remains consistent.
2. Why Use Topological Sort?**
The dependencies between components can be represented as a **Directed Acyclic Graph (DAG)**:
- **Nodes** = components or features.
- **Edges** = dependency relationships (e.g., "Feature B depends on Feature A").
To propagate changes correctly:
- You must update components **in dependency order** (parents before children).
- This is where **topological sorting** comes in—it gives a linear order of nodes such that every dependency comes before the dependent.
3. How It Works**
**Steps:**
1. **Build the dependency graph**:
- For each feature/component, list what it depends on.
2. **Perform topological sort**:
- Use algorithms like **Kahn’s Algorithm** or **DFS-based sort**.
3. **Propagate changes in sorted order**:
- Start from nodes with no dependencies (roots).
- Update each node, then move to its dependents.
4. Example**
Imagine a CAD model:
- **Sketch → Extrude → Fillet → Hole**
- If you change the **Sketch**, the **Extrude**, **Fillet**, and **Hole** must update in that order.
Graph:
Sketch → Extrude → Fillet → Hole
Topological sort result:
[Sketch, Extrude, Fillet, Hole]
Update in this order to maintain consistency.
5. Benefits**
- Prevents circular updates (since DAG ensures no cycles).
- Ensures deterministic and efficient update propagation.
- Scales well for complex assemblies.
*/
var Color;
(function (Color) {
Color[Color["White"] = 0] = "White";
Color[Color["Gray"] = 1] = "Gray";
Color[Color["Black"] = 2] = "Black";
})(Color || (Color = {}));
export class Graph {
_nodes = [];
_edges = new Map();
constructor() {
}
addNode(node) {
if (!this._nodes.includes(node))
this._nodes.push(node);
}
*nodes() {
yield* this._nodes;
}
*edges() {
for (const [from, toList] of this._edges.entries()) {
for (const to of toList) {
yield { from, to };
}
}
}
addEdge(from, to) {
this.addNode(from);
if (!this._edges.has(from)) {
this._edges.set(from, []);
}
if (Array.isArray(to)) {
to.forEach(t => this.addNode(t));
this._edges.get(from).push(...to);
}
else {
this.addNode(to);
this._edges.get(from).push(to);
}
}
getEdges(node) {
if (!this._edges.has(node))
return [];
return this._edges.get(node);
}
clone() {
const newGraph = new Graph();
for (const node of this._nodes) {
newGraph.addNode(node);
}
for (const [from, toList] of this._edges.entries()) {
newGraph.addEdge(from, toList);
}
return newGraph;
}
toGraphvis(accessor) {
// Implementation for converting the graph to Graphviz DOT format
let dot = "digraph G {\n";
for (const node of this._nodes) {
dot += ` "${accessor.getId(node)}" [label="${accessor.getLabel(node)}"];\n`;
}
for (const [from, toList] of this._edges.entries()) {
for (const to of toList) {
dot += ` "${accessor.getId(from)}" -> "${accessor.getId(to)}";\n`;
}
}
dot += "}\n";
return dot;
}
}
export class TopologicalSorter {
static visit(graph, node, colors, sorted, failOnCycles) {
if (colors.get(node) === Color.Gray) {
if (failOnCycles)
throw new Error("Graph has a cycle");
else {
return;
}
}
if (colors.get(node) === Color.White) {
colors.set(node, Color.Gray);
const neighbors = graph.getEdges(node);
for (const neighbor of neighbors) {
this.visit(graph, neighbor, colors, sorted, failOnCycles);
}
colors.set(node, Color.Black);
sorted.push(node);
}
}
static sortDepthFirst(graph, updated, failOnCycles = true) {
const sorted = [];
const colors = new Map(Array.from(graph.nodes()).map((node) => [node, Color.White]));
if (updated) {
// remove duplicate
let filteredUpdated = Array.from(new Set(updated));
filteredUpdated = filteredUpdated.filter(node => colors.get(node) === Color.White);
if (filteredUpdated.length !== updated.length) {
throw new Error("Updated list contains nodes that are not in the graph or have duplicates");
}
if (filteredUpdated.length === 0)
updated = undefined;
else
updated = filteredUpdated;
}
for (const node of updated ?? Array.from(graph.nodes())) {
if (colors.get(node) === Color.White) {
this.visit(graph, node, colors, sorted, failOnCycles);
}
}
return sorted.reverse();
}
static sortBreadthFirst(graph, updated, failOnCycles = true) {
const sorted = [];
const queue = [];
// Vector to store indegree of each vertex
const inDegree = new Map();
for (const node of graph.nodes()) {
inDegree.set(node, 0);
}
for (const edge of graph.edges()) {
inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
}
if (updated) {
// remove duplicate
const filteredUpdated = Array.from(new Set(updated));
if (filteredUpdated.length !== updated.length) {
throw new Error("Updated list contains nodes that are not in the graph or have duplicates");
}
if (filteredUpdated.length === 0)
updated = undefined;
else
updated = filteredUpdated;
}
const startNodes = updated ?? Array.from(graph.nodes());
for (const node of startNodes) {
if (inDegree.get(node) === 0) {
queue.push(node);
}
}
if (startNodes.length === 0) {
throw new Error("Graph has at least one cycle");
}
if (startNodes)
while (queue.length > 0) {
const current = queue.shift();
sorted.push(current);
for (const neighbor of graph.getEdges(current)) {
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
if (inDegree.get(neighbor) === 0) {
queue.push(neighbor);
}
}
}
if (failOnCycles && sorted.length !== Array.from(graph.nodes()).length)
throw new Error("Graph has at least one cycle");
return sorted;
}
static validate(graph, sorted) {
if (sorted.length !== Array.from(graph.nodes()).length) {
return false;
}
const position = new Map();
for (let i = 0; i < sorted.length; i++) {
position.set(sorted[i], i);
}
for (const { from, to } of graph.edges()) {
if (position.get(from) > position.get(to)) {
return false;
}
}
return true;
}
}
class ElementDrivesElementEventMonitor {
iModelDb;
onRootChanged = [];
onAllInputsHandled = [];
onBeforeOutputsHandled = [];
onDeletedDependency = [];
constructor(iModelDb) {
this.iModelDb = iModelDb;
InputDrivesOutput.events.onDeletedDependency.addListener((props) => this.onDeletedDependency.push([this.iModelDb.elements.tryGetElement(props.sourceId)?.userLabel, this.iModelDb.elements.tryGetElement(props.targetId)?.userLabel]));
InputDrivesOutput.events.onRootChanged.addListener((props) => this.onRootChanged.push([this.iModelDb.elements.tryGetElement(props.sourceId)?.userLabel, this.iModelDb.elements.tryGetElement(props.targetId)?.userLabel]));
NodeElement.events.onAllInputsHandled.addListener((id) => this.onAllInputsHandled.push(this.iModelDb.elements.tryGetElement(id)?.userLabel));
NodeElement.events.onBeforeOutputsHandled.addListener((id) => this.onBeforeOutputsHandled.push(this.iModelDb.elements.tryGetElement(id)?.userLabel));
}
clear() {
this.onRootChanged.length = 0;
this.onAllInputsHandled.length = 0;
this.onBeforeOutputsHandled.length = 0;
this.onDeletedDependency.length = 0;
}
}
export class InputDrivesOutput extends ElementDrivesElement {
static events = {
onRootChanged: new BeEvent(),
onDeletedDependency: new BeEvent(),
};
static get className() { return "InputDrivesOutput"; }
constructor(props, iModel) {
super(props, iModel);
}
static onRootChanged(props, iModel) {
this.events.onRootChanged.raiseEvent(props, iModel);
}
static onDeletedDependency(props, iModel) {
this.events.onDeletedDependency.raiseEvent(props, iModel);
}
}
export class NodeElement extends GeometricElement3d {
op;
val;
static events = {
onAllInputsHandled: new BeEvent(),
onBeforeOutputsHandled: new BeEvent(),
};
static get className() { return "Node"; }
constructor(props, iModel) {
super(props, iModel);
this.op = props.op;
this.val = props.val;
}
toJSON() {
const val = super.toJSON();
val.op = this.op;
val.val = this.val;
return val;
}
static onAllInputsHandled(id, iModel) {
this.events.onAllInputsHandled.raiseEvent(id, iModel);
}
static onBeforeOutputsHandled(id, iModel) {
this.events.onBeforeOutputsHandled.raiseEvent(id, iModel);
}
static generateGeometry(radius) {
const builder = new GeometryStreamBuilder();
const p1 = Point3d.createZero();
const p2 = Point3d.createFrom({ x: radius, y: 0.0, z: 0.0 });
const circle = LineSegment3d.create(p1, p2);
builder.appendGeometry(circle);
return builder.geometryStream;
}
static getCategory(iModelDb) {
const categoryId = SpatialCategory.queryCategoryIdByName(iModelDb, IModelDb.dictionaryId, this.classFullName);
if (categoryId === undefined)
throw new IModelError(IModelStatus.NotFound, "Category not found");
return iModelDb.elements.getElement(categoryId);
}
}
export class NetworkSchema extends Schema {
static get schemaName() { return "Network"; }
static registerSchema() {
if (this !== Schemas.getRegisteredSchema(NetworkSchema.schemaName)) {
Schemas.registerSchema(this);
ClassRegistry.register(NodeElement, this);
ClassRegistry.register(InputDrivesOutput, this);
}
}
static async importSchema(iModel) {
if (iModel.querySchemaVersion("Network"))
return;
const schema1 = `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="Network" alias="net" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
<ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/>
<ECEntityClass typeName="Node">
<BaseClass>bis:GraphicalElement3d</BaseClass>
<ECProperty propertyName="op" typeName="string" />
<ECProperty propertyName="val" typeName="double" />
</ECEntityClass>
<ECRelationshipClass typeName="InputDrivesOutput" modifier="None" strength="referencing">
<BaseClass>bis:ElementDrivesElement</BaseClass>
<Source multiplicity="(0..1)" roleLabel="drives" polymorphic="true">
<Class class="Node"/>
</Source>
<Target multiplicity="(0..*)" roleLabel="is driven by" polymorphic="false">
<Class class="Node"/>
</Target>
<ECProperty propertyName="prop" typeName="double" />
</ECRelationshipClass>
</ECSchema>`;
await iModel.importSchemaStrings([schema1]);
}
}
export class Engine {
static async createGraph(iModelDb, modelId, graph) {
const nodes = new Map();
const outGraph = new Graph();
for (const node of graph.nodes()) {
const id = await this.insertNode(iModelDb, modelId, node, "", 0, new Point3d(0, 0, 0));
nodes.set(node, { id, name: node });
}
for (const edge of graph.edges()) {
const fromId = nodes.get(edge.from).id;
const toId = nodes.get(edge.to).id;
await this.insertEdge(iModelDb, fromId, toId, 0);
outGraph.addEdge(nodes.get(edge.from), nodes.get(edge.to));
}
return outGraph;
}
static countNodes(iModelDb) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return iModelDb.withPreparedStatement("SELECT COUNT(*) FROM Network.Node", (stmt) => {
if (stmt.step() === DbResult.BE_SQLITE_ROW) {
return stmt.getValue(0).getInteger();
}
return 0;
});
}
static countEdges(iModelDb) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return iModelDb.withPreparedStatement("SELECT COUNT(*) FROM [Network].[InputDrivesOutput]", (stmt) => {
if (stmt.step() === DbResult.BE_SQLITE_ROW) {
return stmt.getValue(0).getInteger();
}
return 0;
});
}
static queryEdgesForSource(iModelDb, sourceId) {
const edges = [];
// eslint-disable-next-line @typescript-eslint/no-deprecated
iModelDb.withPreparedStatement("SELECT [ECInstanceId], [SourceECInstanceId], [TargetECInstanceId], [prop], [Status], [Priority] FROM [Network].[InputDrivesOutput] WHERE [SourceECInstanceId] = ?", (stmt) => {
stmt.bindId(1, sourceId);
while (stmt.step() === DbResult.BE_SQLITE_ROW) {
edges.push({
id: stmt.getValue(0).getId(),
classFullName: InputDrivesOutput.classFullName,
sourceId: stmt.getValue(1).getId(),
targetId: stmt.getValue(2).getId(),
prop: stmt.getValue(3).getDouble(),
status: stmt.getValue(4).getInteger(),
priority: stmt.getValue(5).getInteger(),
});
}
});
return edges;
}
static queryEdgesForTarget(iModelDb, targetId) {
const edges = [];
// eslint-disable-next-line @typescript-eslint/no-deprecated
iModelDb.withPreparedStatement("SELECT [ECInstanceId], [SourceECInstanceId], [TargetECInstanceId], [prop], [Status], [Priority] FROM [Network].[InputDrivesOutput] WHERE [TargetECInstanceId] = ?", (stmt) => {
stmt.bindId(1, targetId);
while (stmt.step() === DbResult.BE_SQLITE_ROW) {
edges.push({
id: stmt.getValue(0).getId(),
classFullName: InputDrivesOutput.classFullName,
sourceId: stmt.getValue(1).getId(),
targetId: stmt.getValue(2).getId(),
prop: stmt.getValue(3).getDouble(),
status: stmt.getValue(4).getInteger(),
priority: stmt.getValue(5).getInteger(),
});
}
});
return edges;
}
static async createPartition(iModelDb) {
const parentId = new SubjectOwnsPartitionElements(IModel.rootSubjectId);
const modelId = IModel.repositoryModelId;
const modeledElementProps = {
classFullName: PhysicalPartition.classFullName,
parent: parentId,
model: modelId,
code: Code.createEmpty(),
userLabel: "NetworkPhysicalPartition"
};
const modeledElement = iModelDb.elements.createElement(modeledElementProps);
await iModelDb.locks.acquireLocks({ shared: modelId });
return iModelDb.elements.insertElement(modeledElement.toJSON());
}
static async createModel(iModelDb) {
const partitionId = await this.createPartition(iModelDb);
const modeledElementRef = new RelatedElement({ id: partitionId });
const newModel = iModelDb.models.createModel({ modeledElement: modeledElementRef, classFullName: PhysicalModel.classFullName });
const newModelId = newModel.insert();
return newModelId;
}
static async createNodeCategory(iModelDb) {
const category = SpatialCategory.create(iModelDb, IModelDb.dictionaryId, NodeElement.classFullName);
return category.insert();
}
static async initialize(iModelDb) {
await NetworkSchema.importSchema(iModelDb);
NetworkSchema.registerSchema();
const modelId = await this.createModel(iModelDb);
const categoryId = await this.createNodeCategory(iModelDb);
return {
modelId,
categoryId,
};
}
static async insertNode(iModelDb, modelId, name, op, val, location, radius = 0.1) {
const props = {
classFullName: NodeElement.classFullName,
model: modelId,
code: Code.createEmpty(),
userLabel: name,
category: NodeElement.getCategory(iModelDb).id,
placement: { origin: location, angles: new YawPitchRollAngles() },
geom: NodeElement.generateGeometry(radius),
op,
val,
};
await iModelDb.locks.acquireLocks({ shared: modelId });
return iModelDb.elements.insertElement(props);
}
static async deleteNode(iModelDb, nodeId) {
await iModelDb.locks.acquireLocks({ exclusive: nodeId });
return iModelDb.elements.deleteElement(nodeId);
}
static async updateNodeProps(iModelDb, props) {
await iModelDb.locks.acquireLocks({ exclusive: props.id });
return iModelDb.elements.updateElement(props);
}
static async updateNode(iModelDb, userLabel) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const id = iModelDb.withPreparedStatement("SELECT [ECInstanceId] FROM [Network].[Node] WHERE [UserLabel] = ?", (stmt) => {
stmt.bindString(1, userLabel);
if (stmt.step() === DbResult.BE_SQLITE_ROW)
return stmt.getValue(0).getId();
return undefined;
});
if (!id) {
throw new Error(`Node with userLabel ${userLabel} not found`);
}
await this.updateNodeProps(iModelDb, { id });
}
static async deleteEdge(iModelDb, from, to) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const edge = iModelDb.withPreparedStatement(`
SELECT [IDo].[ECInstanceId], [IDo].[SourceECInstanceId], [IDo].[TargetECInstanceId]
FROM [Network].[InputDrivesOutput] [IDo]
JOIN [Network].[Node] [Src] ON [Src].[ECInstanceId] = [IDo].[SourceECInstanceId]
JOIN [Network].[Node] [Tgt] ON [Tgt].[ECInstanceId] = [IDo].[TargetECInstanceId]
WHERE [Src].[UserLabel] = ? AND [Tgt].[UserLabel] = ?`, (stmt) => {
stmt.bindString(1, from);
stmt.bindString(2, to);
if (stmt.step() === DbResult.BE_SQLITE_ROW)
return {
id: stmt.getValue(0).getId(),
classFullName: InputDrivesOutput.classFullName,
sourceId: stmt.getValue(1).getId(),
targetId: stmt.getValue(2).getId(),
};
return undefined;
});
if (!edge) {
throw new Error(`Edge from ${from} to ${to} not found`);
}
iModelDb.relationships.deleteInstance(edge);
}
static async insertEdge(iModelDb, sourceId, targetId, prop) {
const props = {
classFullName: InputDrivesOutput.classFullName,
sourceId,
targetId,
prop,
status: 0,
priority: 0
};
return iModelDb.relationships.insertInstance(props);
}
}
describe("ElementDrivesElement Tests", () => {
const briefcases = [];
let iModelId;
async function openBriefcase() {
const iModelDb = await HubWrappers.downloadAndOpenBriefcase({ iTwinId: HubMock.iTwinId, iModelId });
iModelDb.channels.addAllowedChannel(ChannelControl.sharedChannelName);
iModelDb.saveChanges();
briefcases.push(iModelDb);
return iModelDb;
}
beforeEach(async () => {
HubMock.startup("TestIModel", KnownTestLocations.outputDir);
iModelId = await HubMock.createNewIModel({ iTwinId: HubMock.iTwinId, iModelName: "Test", description: "TestSubject" });
});
afterEach(async () => {
NodeElement.events.onAllInputsHandled.clear();
NodeElement.events.onBeforeOutputsHandled.clear();
InputDrivesOutput.events.onRootChanged.clear();
InputDrivesOutput.events.onDeletedDependency.clear();
for (const briefcase of briefcases) {
briefcase.close();
}
HubMock.shutdown();
});
it("local: topological sort", async () => {
const graph = new Graph();
graph.addEdge("1", ["2", "3"]);
graph.addEdge("2", ["5", "4"]);
graph.addEdge("3", ["4"]);
graph.addEdge("4", ["5"]);
const df = TopologicalSorter.sortDepthFirst(graph);
chai.expect(TopologicalSorter.validate(graph, df)).to.be.true;
chai.expect(df).to.deep.equal(["1", "3", "2", "4", "5"]);
const bf = TopologicalSorter.sortBreadthFirst(graph);
chai.expect(TopologicalSorter.validate(graph, bf)).to.be.true;
chai.expect(bf).to.deep.equal(["1", "2", "3", "4", "5"]);
});
it("local: cycle detection (suppress cycles)", async () => {
const graph = new Graph();
// Graph structure:
// 1 --> 2 <-- 3
// ^ |
// |-----------|
graph.addEdge("1", ["2"]);
graph.addEdge("2", ["3"]);
graph.addEdge("3", ["1"]);
const df = TopologicalSorter.sortDepthFirst(graph, [], false);
chai.expect(TopologicalSorter.validate(graph, df)).to.be.false;
chai.expect(df).to.deep.equal(["1", "2", "3"]);
});
it("local: cycle detection (throw)", async () => {
const graph = new Graph();
// Graph structure:
// 1 --> 2 --> 3
// ^ |
// |-----------|
graph.addEdge("1", ["2"]);
graph.addEdge("2", ["3"]);
graph.addEdge("3", ["1"]);
chai.expect(() => TopologicalSorter.sortDepthFirst(graph)).to.throw("Graph has a cycle");
});
it("EDE/local: build system dependencies", async () => {
const graph = new Graph();
/*
Example: Build system dependencies
- Compile main.c and util.c to main.o and util.o
- Link main.o and util.o to produce app.exe
- app.exe depends on config.json
- test.exe depends on main.o, util.o, and test.c
Graph:
main.c util.c test.c config.json
| | | |
v v | |
main.o util.o | |
\ / | |
\ / | |
app.exe test.exe |
| | |
+----------------+---------+
*/
graph.addEdge("main.c", ["main.o"]);
graph.addEdge("util.c", ["util.o"]);
graph.addEdge("test.c", ["test.exe"]);
graph.addEdge("main.o", ["app.exe", "test.exe"]);
graph.addEdge("util.o", ["app.exe", "test.exe"]);
graph.addEdge("config.json", ["app.exe", "test.exe"]);
// create graph
const b1 = await openBriefcase();
const { modelId, } = await Engine.initialize(b1);
const monitor = new ElementDrivesElementEventMonitor(b1);
await Engine.createGraph(b1, modelId, graph);
b1.saveChanges();
b1.saveChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([
["main.c", "main.o"],
["main.o", "test.exe"],
["main.o", "app.exe"],
["util.c", "util.o"],
["util.o", "test.exe"],
["util.o", "app.exe"],
["test.c", "test.exe"],
["config.json", "test.exe"],
["config.json", "app.exe"],
]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["main.o", "util.o", "test.exe", "app.exe"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["main.c", "util.c", "test.c", "config.json"]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
// update main.c
monitor.clear();
await Engine.updateNode(b1, "main.c");
b1.saveChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([
["main.c", "main.o"],
["main.o", "test.exe"],
["main.o", "app.exe"],
]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["main.o", "test.exe", "app.exe"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["main.c"]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
// Topological sort (depth-first)
const df = TopologicalSorter.sortDepthFirst(graph);
chai.expect(TopologicalSorter.validate(graph, df)).to.be.true;
// Topological sort (breadth-first)
const bf = TopologicalSorter.sortBreadthFirst(graph);
chai.expect(TopologicalSorter.validate(graph, bf)).to.be.true;
chai.expect(df).to.deep.equal(["config.json", "test.c", "util.c", "util.o", "main.c", "main.o", "test.exe", "app.exe"]);
chai.expect(bf).to.deep.equal(["main.c", "util.c", "test.c", "config.json", "main.o", "util.o", "app.exe", "test.exe"]);
});
it("EDE/local: complex, subset", async () => {
const graph = new Graph();
/*
Graph shows what must be put on before other items:
- Socks before Shoes
- Underwear before Shoes and Pants
- Pants before Belt and Shoes
- Shirt before Belt and Tie
- Tie before Jacket
- Belt before Jacket
- Watch has no dependencies
*/
graph.addEdge("Socks", ["Shoes"]);
graph.addEdge("Underwear", ["Shoes", "Pants"]);
graph.addEdge("Pants", ["Belt", "Shoes"]);
graph.addEdge("Shirt", ["Belt", "Tie"]);
graph.addEdge("Tie", ["Jacket"]);
graph.addEdge("Belt", ["Jacket"]);
graph.addNode("Watch");
// Test using EDE
const b1 = await openBriefcase();
const { modelId, } = await Engine.initialize(b1);
const monitor = new ElementDrivesElementEventMonitor(b1);
await Engine.createGraph(b1, modelId, graph);
b1.saveChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([
["Socks", "Shoes"],
["Underwear", "Shoes"],
["Underwear", "Pants"],
["Pants", "Shoes"],
["Pants", "Belt"],
["Shirt", "Belt"],
["Belt", "Jacket"],
["Shirt", "Tie"],
["Tie", "Jacket"]
]);
// Watch is missing as it is not connected to any other node.
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["Pants", "Shoes", "Belt", "Tie", "Jacket"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["Socks", "Underwear", "Shirt"]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
monitor.clear();
await Engine.updateNode(b1, "Socks");
b1.saveChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([["Socks", "Shoes"]]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["Shoes"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["Socks"]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
const sorted = TopologicalSorter.sortDepthFirst(graph);
chai.expect(TopologicalSorter.validate(graph, sorted)).to.be.true;
chai.expect(sorted).to.deep.equal(["Watch", "Shirt", "Tie", "Underwear", "Pants", "Belt", "Jacket", "Socks", "Shoes"]);
// Test sorting with a subset of nodes
const sorted1 = TopologicalSorter.sortDepthFirst(graph, ["Underwear"]);
chai.expect(sorted1).to.deep.equal(["Underwear", "Pants", "Belt", "Jacket", "Shoes"]);
const sorted2 = TopologicalSorter.sortDepthFirst(graph, ["Belt"]);
chai.expect(sorted2).to.deep.equal(["Belt", "Jacket"]);
const sorted3 = TopologicalSorter.sortDepthFirst(graph, ["Shoes"]);
chai.expect(sorted3).to.deep.equal(["Shoes"]);
const sorted4 = TopologicalSorter.sortDepthFirst(graph, ["Socks"]);
chai.expect(sorted4).to.deep.equal(["Socks", "Shoes"]);
const sorted5 = TopologicalSorter.sortDepthFirst(graph, ["Tie"]);
chai.expect(sorted5).to.deep.equal(["Tie", "Jacket"]);
const sorted6 = TopologicalSorter.sortDepthFirst(graph, ["Jacket"]);
chai.expect(sorted6).to.deep.equal(["Jacket"]);
const sorted7 = TopologicalSorter.sortDepthFirst(graph, ["Shirt"]);
chai.expect(sorted7).to.deep.equal(["Shirt", "Tie", "Belt", "Jacket"]);
const sorted8 = TopologicalSorter.sortDepthFirst(graph, ["Watch"]);
chai.expect(sorted8).to.deep.equal(["Watch"]);
});
it("EDE: basic graph operations", async () => {
const b1 = await openBriefcase();
const { modelId, } = await Engine.initialize(b1);
const graph = new Graph();
// Graph structure:
// A
// / \
// B C
// |\ /
// | \/
// E--D
graph.addEdge("A", ["B", "C"]);
graph.addEdge("B", ["E", "D"]);
graph.addEdge("C", ["D"]);
graph.addEdge("D", ["E"]);
const monitor = new ElementDrivesElementEventMonitor(b1);
// create a network
await Engine.createGraph(b1, modelId, graph);
b1.saveChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([
["A", "B"],
["A", "C"],
["B", "E"],
["B", "D"],
["C", "D"],
["D", "E"]
]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["B", "C", "D", "E"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["A"]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
monitor.clear();
// update a node in network
await Engine.updateNode(b1, "B");
b1.saveChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([
["B", "E"],
["B", "D"],
["D", "E"]
]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["D", "E"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal(["B"]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
monitor.clear();
// delete edge in network
await Engine.deleteEdge(b1, "B", "E");
b1.saveChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal([]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([["B", "E"]]);
});
it("EDE: cyclical throw exception", async () => {
const b1 = await openBriefcase();
const { modelId, } = await Engine.initialize(b1);
const graph = new Graph();
// Graph structure with a cycle:
// A
// / \
// B - C
graph.addEdge("A", ["B"]);
graph.addEdge("B", ["C"]);
graph.addEdge("C", ["A"]);
const monitor = new ElementDrivesElementEventMonitor(b1);
// create a network
await Engine.createGraph(b1, modelId, graph);
chai.expect(() => b1.saveChanges()).to.throw("Could not save changes due to propagation failure.");
b1.abandonChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([["B", "C"], ["C", "A"], ["A", "B"]]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["C", "A", "B"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
monitor.clear();
});
it("EDE: cyclical graph can start propagation with no clear starting element", async () => {
const b1 = await openBriefcase();
const { modelId, } = await Engine.initialize(b1);
const graph = new Graph();
// Graph structure with a cycle:
// A
// / \
// B - C
// order of insertion effect graph with cycles.
graph.addNode("B");
graph.addNode("A");
graph.addNode("C");
graph.addEdge("A", ["B"]);
graph.addEdge("B", ["C"]);
graph.addEdge("C", ["A"]);
const monitor = new ElementDrivesElementEventMonitor(b1);
// create a network
await Engine.createGraph(b1, modelId, graph);
chai.expect(() => b1.saveChanges()).to.throw("Could not save changes due to propagation failure.");
b1.abandonChanges();
chai.expect(monitor.onRootChanged).to.deep.equal([["C", "A"], ["A", "B"], ["B", "C"]]);
chai.expect(monitor.onAllInputsHandled).to.deep.equal(["A", "B", "C"]);
chai.expect(monitor.onBeforeOutputsHandled).to.deep.equal([]);
chai.expect(monitor.onDeletedDependency).to.deep.equal([]);
monitor.clear();
});
it.skip("EDE: performance", async () => {
const b1 = await openBriefcase();
const { modelId, } = await Engine.initialize(b1);
const graph = new Graph();
const createTree = (depth, breadth, prefix) => {
if (depth === 0)
return;
for (let i = 0; i < breadth; i++) {
const node = `${prefix}${i}`;
graph.addNode(node);
if (depth > 1) {
for (let j = 0; j < breadth; j++) {
const child = `${prefix}${i}${j}`;
graph.addEdge(node, [child]);
createTree(depth - 1, breadth, `${prefix}${i}${j}`);
}
}
}
};
const stopWatch0 = new StopWatch("create graph", true);
createTree(5, 3, "N");
await Engine.createGraph(b1, modelId, graph);
stopWatch0.stop();
const createGraphTime = stopWatch0.elapsed.seconds;
let onRootChangedCount = 0;
let onDeletedDependencyCount = 0;
let onAllInputsHandledCount = 0;
let onBeforeOutputsHandledCount = 0;
InputDrivesOutput.events.onRootChanged.addListener(() => { onRootChangedCount++; });
InputDrivesOutput.events.onDeletedDependency.addListener(() => { onDeletedDependencyCount++; });
NodeElement.events.onAllInputsHandled.addListener((_id) => { onAllInputsHandledCount++; });
NodeElement.events.onBeforeOutputsHandled.addListener(() => { onBeforeOutputsHandledCount++; });
const stopWatch1 = new StopWatch("save changes", true);
b1.saveChanges();
stopWatch1.stop();
const saveChangesTime = stopWatch1.elapsed.seconds;
chai.expect(onRootChangedCount).to.be.equals(7380);
chai.expect(onDeletedDependencyCount).to.equal(0);
chai.expect(onAllInputsHandledCount).to.be.equals(7380);
chai.expect(onBeforeOutputsHandledCount).to.equal(2460);
chai.expect(createGraphTime).to.be.lessThan(3);
chai.expect(saveChangesTime).to.be.lessThan(3);
});
});
//# sourceMappingURL=ElementDrivesElement.test.js.map