@gravity-ui/graph
Version:
Modern graph editor component
134 lines (133 loc) • 4.03 kB
JavaScript
import { signal } from "@preact/signals-core";
export const IS_PORT_TYPE = "Port";
/**
* PortState - Reactive state container for a connection port
*
* Manages the lifecycle and state of a port, including position updates,
* component ownership, and listener management for connections that use this port.
*
* ## Key Concepts:
*
* ### Lazy Creation
* Ports are created on-demand when connections need them, even if the target
* component doesn't exist yet. This solves initialization order problems.
*
* ### Lookup State
* When `lookup: true`, the port is waiting for its component to provide coordinates.
* When `lookup: false`, the port has valid coordinates and can be used for rendering.
*
* ### Listener Management
* Tracks which components are listening to this port's changes. When no listeners
* remain and no component owns the port, it can be safely garbage collected.
*/
export class PortState {
/**
* Get the port's unique identifier
*
* @returns {TPortId} The port's ID
*/
get id() {
return this.$state.value.id;
}
/**
* Get the port's X coordinate
*
* @returns {number} The X coordinate
*/
get x() {
return this.$state.value.x;
}
/**
* Get the port's Y coordinate
*
* @returns {number} The Y coordinate
*/
get y() {
return this.$state.value.y;
}
/**
* Get the component that owns this port
*
* @returns {Component | undefined} The owning component, if any
*/
get component() {
return this.owner || this.$state.value.component;
}
/**
* Get whether the port is in lookup state (waiting for coordinates)
*
* @returns {boolean | undefined} True if waiting for coordinates, false if resolved
*/
get lookup() {
return this.$state.value.lookup;
}
constructor(port) {
this.$state = signal(undefined);
/**
* Set of references observing this port's changes
*
* Used for reference counting to determine when the port can be safely deleted.
* Stores actual object references to ensure accurate counting and prevent duplicates.
*/
this.observers = new Set();
this.$state.value = { ...port };
// Initialize owner if component was provided in the constructor
if (port.component) {
this.owner = port.component;
}
}
/**
* Set the component that owns this port
* @param owner Component that will own this port (block, anchor, etc.)
* @returns void
*/
setOwner(owner) {
this.owner = owner;
this.updatePort({ component: owner, lookup: false });
}
/**
* Remove the current owner from this port
*/
removeOwner() {
this.owner = undefined;
this.updatePort({ component: undefined, lookup: true });
}
/**
* Add an observer reference to this port
* Stores the actual reference for accurate counting
* @param observer The object observing this port
*/
addObserver(observer) {
this.observers.add(observer);
}
/**
* Remove an observer reference from this port
* Removes the actual reference from the set
* @param observer The object to stop observing this port
*/
removeObserver(observer) {
this.observers.delete(observer);
}
/**
* Update the port's position coordinates
* @param x New X coordinate
* @param y New Y coordinate
*/
setPoint(x, y) {
this.updatePort({ x, y });
}
/**
* Update port state with partial data
* @param port Partial port data to merge with current state
*/
updatePort(port) {
this.$state.value = { ...this.$state.value, ...port };
}
/**
* Check if this port can be safely deleted
* @returns true if port has no owner and no observers
*/
canBeDeleted() {
return !this.owner && this.observers.size === 0;
}
}