diff --git a/src/store/connection.ts b/src/store/connection.ts index 9089ba7..99f4545 100644 --- a/src/store/connection.ts +++ b/src/store/connection.ts @@ -137,10 +137,15 @@ export class CCConnectionStore extends EventEmitter { this.emit("willUnregister", connection); this.#connections.delete(connection.id); } + // Announced as soon as the connections are gone, not when the transaction ends. + // Otherwise a listener of a change announced in between, such as a renderer + // subscribed to the node at one end, would look up a connection that is gone + // while the list of connections still has it. Removing a connection cascades + // to nothing, so its listeners see no half-removed state. + for (const connection of connections) { + this.emit("didUnregister", connection); + } }); - for (const connection of connections) { - this.emit("didUnregister", connection); - } } /** diff --git a/src/store/nodePin.test.ts b/src/store/nodePin.test.ts index d723b2c..ddf6f4c 100644 --- a/src/store/nodePin.test.ts +++ b/src/store/nodePin.test.ts @@ -148,6 +148,44 @@ describe("Node pin store", () => { ).toEqual({ isFixed: true, bitWidth: 8 }); }); + it("should drop a removed input pin of an aggregate node from its output right away", () => { + const { store, rootComponent } = createRootStore(); + const node = registerNode(store, rootComponent, intrinsics.aggregate); + const firstInputNodePin = nullthrows( + store.nodePins + .getManyByNodeId(node.id) + .find( + (pin) => pin.componentPinId === intrinsics.aggregate.inputPin.In.id, + ), + ); + store.nodePins.update(firstInputNodePin.id, { manualBitWidth: 5 }); + const secondInputNodePin = CCNodePinStore.create({ + nodeId: node.id, + componentPinId: intrinsics.aggregate.inputPin.In.id, + order: firstInputNodePin.order + 1, + manualBitWidth: 3, + }); + store.nodePins.register(secondInputNodePin); + expect( + getNodePinBitWidth( + store, + node.id, + intrinsics.aggregate.outputPin.Out.id, + ), + ).toEqual({ isFixed: true, bitWidth: 8 }); + + // The removal is announced only when the transaction ends, but the pin is gone now + store.nodePins.unregister(secondInputNodePin.id); + + expect( + getNodePinBitWidth( + store, + node.id, + intrinsics.aggregate.outputPin.Out.id, + ), + ).toEqual({ isFixed: true, bitWidth: 5 }); + }); + it("should sum up the manual bit widths of the output pins of a decompose node", () => { const { store, rootComponent } = createRootStore(); const node = registerNode(store, rootComponent, intrinsics.decompose); @@ -271,6 +309,23 @@ describe("Node pin store", () => { expect(store.connections.getMany()).toHaveLength(0); }); + it("should announce a dropped connection before the config change that drops it", () => { + const { store, displayNode } = connectBroadcastToDisplay(20 * 15); + const events: string[] = []; + store.connections.on("didUnregister", () => + events.push("connection removed"), + ); + store.nodes.on("didUpdate", () => events.push("node updated")); + + // A renderer of the connection subscribed to the display re-renders on "node + // updated", so by then the list of connections must no longer contain it + store.nodes.update(displayNode.id, { + config: { resolution: { x: 4, y: 3 } }, + }); + + expect(events).toEqual(["connection removed", "node updated"]); + }); + it("should keep the connections that stay consistent across a display config change", () => { const { store, displayNode } = connectBroadcastToDisplay(20 * 15); diff --git a/src/store/nodePin.ts b/src/store/nodePin.ts index 0b00c77..90ee596 100644 --- a/src/store/nodePin.ts +++ b/src/store/nodePin.ts @@ -130,8 +130,10 @@ export class CCNodePinStore extends EventEmitter { await this.#store.transactionManager.runInTransaction(() => { this.emit("willUnregister", nodePin); this.#nodePins.delete(nodePin.id); + // Its removal is announced only when the transaction ends, but the bit widths + // derived from it are stale from now on + this.#clearBitWidthCache(); }); - this.#clearBitWidthCache(); this.emit("didUnregister", nodePin); this.#markedAsDeleted.delete(id); }