Grow a node with its ports, stop it flickering, draw what it reaches out to
- A node's height follows the ports on its busiest side. It is a function of the document, so `layoutGraph` reserves exactly what is drawn and nothing measured is fed back into the layout. - The three status controls now sit in slots that are there whether the control is or not. A node running many times a second mounted and unmounted the stop button on every execution, resizing the card each time. - A port bound to another flow's message is drawn as a label, naming the node at the far end and its type. Only the opposite direction was answered before. The scan behind both is now cached on the store's commit counter rather than reading every flow per request. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
This commit is contained in:
@@ -113,6 +113,17 @@ class RunContext:
|
||||
run_id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Wiring:
|
||||
"""One published node and the qualified messages it is bound to."""
|
||||
|
||||
flow: str
|
||||
node: str
|
||||
type: str
|
||||
provides: list[str]
|
||||
requires: list[str]
|
||||
|
||||
|
||||
class EmitSink:
|
||||
"""Turns a worker's mid-call frames back into the node's own outputs.
|
||||
|
||||
@@ -315,6 +326,9 @@ class FlowController:
|
||||
self.history_limits: dict[str, int] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._failures: asyncio.Task[None] | None = None
|
||||
#: Every published node's ports, with the store revision it was read
|
||||
#: at — see `_wiring`.
|
||||
self._wiring_cache: tuple[int, list[Wiring]] | None = None
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
@@ -843,6 +857,34 @@ class FlowController:
|
||||
)
|
||||
return infos
|
||||
|
||||
def _wiring(self) -> list[Wiring]:
|
||||
"""Every published node and the qualified messages it is bound to.
|
||||
|
||||
Opening one flow asks what every other flow is wired to, in both
|
||||
directions, which otherwise reads and parses every flow on the disk per
|
||||
request. Rebuilt when the store commits — see `FlowStore.revision` —
|
||||
rather than cached with a lifetime, so a save is visible immediately.
|
||||
"""
|
||||
revision = self.store.revision
|
||||
if self._wiring_cache is not None and self._wiring_cache[0] == revision:
|
||||
return self._wiring_cache[1]
|
||||
|
||||
index: list[Wiring] = []
|
||||
for other in self.store.read_all():
|
||||
for node in other.nodes:
|
||||
provides = sorted(
|
||||
qualify(other.name, spec.name) for spec in _bound(node.provides)
|
||||
)
|
||||
requires = sorted(
|
||||
qualify(other.name, spec.name) for spec in _bound(node.requires)
|
||||
)
|
||||
if provides or requires:
|
||||
index.append(
|
||||
Wiring(other.name, node.id, node.type, provides, requires)
|
||||
)
|
||||
self._wiring_cache = (revision, index)
|
||||
return index
|
||||
|
||||
def cross_flow_nodes(
|
||||
self, flow: str
|
||||
) -> list[tuple[str, str, list[str], list[str]]]:
|
||||
@@ -854,24 +896,33 @@ class FlowController:
|
||||
"""
|
||||
prefix = f"{flow}."
|
||||
found = []
|
||||
for other in self.store.read_all():
|
||||
if other.name == flow:
|
||||
for entry in self._wiring():
|
||||
if entry.flow == flow:
|
||||
continue
|
||||
for node in other.nodes:
|
||||
provides = sorted(
|
||||
qualify(other.name, spec.name)
|
||||
for spec in _bound(node.provides)
|
||||
if qualify(other.name, spec.name).startswith(prefix)
|
||||
)
|
||||
requires = sorted(
|
||||
qualify(other.name, spec.name)
|
||||
for spec in _bound(node.requires)
|
||||
if qualify(other.name, spec.name).startswith(prefix)
|
||||
)
|
||||
if provides or requires:
|
||||
found.append((other.name, node.id, provides, requires))
|
||||
provides = [name for name in entry.provides if name.startswith(prefix)]
|
||||
requires = [name for name in entry.requires if name.startswith(prefix)]
|
||||
if provides or requires:
|
||||
found.append((entry.flow, entry.node, provides, requires))
|
||||
return found
|
||||
|
||||
def message_node(
|
||||
self, message: str, published: bool, exclude: str = ""
|
||||
) -> Wiring | None:
|
||||
"""The published node on the far side of ``message``.
|
||||
|
||||
``published`` asks for the one that publishes it, otherwise for one
|
||||
that reads it, and ``exclude`` names the flow doing the asking — whose
|
||||
own nodes are never the far side of anything. A message may have
|
||||
several readers; the first stands for them, which is all a label naming
|
||||
the other end has to say.
|
||||
"""
|
||||
for entry in self._wiring():
|
||||
if entry.flow == exclude:
|
||||
continue
|
||||
if message in (entry.provides if published else entry.requires):
|
||||
return entry
|
||||
return None
|
||||
|
||||
def brain_graph(self) -> BrainGraph:
|
||||
"""Every published flow as one graph, merged on what its nodes talk to.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user