UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

134 lines (133 loc) 4.03 kB
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; } }