connector: wanted(port) says whether anyone watches or reads a port

This commit is contained in:
Melvin Strobl
2026-10-27 15:20:24 +01:00
parent 7b453af3fe
commit 00cda9d2f1
4 changed files with 68 additions and 0 deletions
+14
View File
@@ -21,6 +21,8 @@ What a connector gets from the base class:
* :meth:`ConnectorNode.save_artifact`, for a device whose readings are bytes —
a camera frame, a recorded clip — which travel as a reference on a media
port rather than as the message itself;
* :meth:`ConnectorNode.wanted`, so a camera fetches frames only while someone
watches them or a running flow reads them;
* :attr:`ConnectorNode.store`, for the few short strings that have to outlive
a restart — a certificate pinned on first contact, a pairing token — shared
by every node of the connector's package; :mod:`fluksio.flow.connector_tls`
@@ -40,6 +42,7 @@ from typing import TYPE_CHECKING, Any, ClassVar
from pydantic import BaseModel, Field
from fluksio.flow.connector_store import ConnectorStore, namespace_of
from fluksio.flow.demand import demand
from fluksio.flow.nodes import Node
if TYPE_CHECKING:
@@ -173,6 +176,17 @@ class ConnectorNode(Node):
[data], name=name, media_type=media_type, volatile=volatile
)
def wanted(self, port: str) -> bool:
"""Whether anyone wants what ``port`` publishes right now.
A dashboard tile on screen, a screen through the portal, or a node of a
running flow reading the port's message. A camera asks before each
frame, so it sends nothing while nobody watches. Always true on a
remote worker, which cannot see who watches.
"""
names = {s.name for s in self.output_ports if s.port == port and s.name}
return demand.wants(names)
@property
def store(self) -> ConnectorStore:
"""Short strings this connector keeps across restarts.
+14
View File
@@ -58,6 +58,20 @@ class Demand:
with self._lock:
return set().union(*self._wanted.values())
def wants(self, names: set[str]) -> bool:
"""Whether a socket views, or a running flow reads, any of ``names``.
What a connector's camera asks before it fetches a frame. True while
nothing is bound, which is a worker process: the sockets are in the
engine's, out of its sight.
"""
controller = self._controller
if controller is None:
# ponytail: a remote worker always sends; forward demand to workers
# when a camera placed on one costs too much.
return True
return bool(names & (self.viewed() | controller.consumed_messages()))
def ports(self, device_id: str) -> list[str]:
"""The device's ports that have demand, sorted."""
return list(self._compute().get(device_id, ()))
+26
View File
@@ -306,3 +306,29 @@ def test_a_connector_publishes_bytes_as_a_media_reference(tmp_path):
# point of a media dtype.
MessageSpec(name="demo.frame", dtype=DType.IMAGE).check(reference)
assert store.path(reference["digest"]).read_bytes() == b"\x89PNG..."
def test_a_port_is_wanted_while_watched_or_read(monkeypatch):
"""A camera fetches frames only while a screen shows them or a flow reads them."""
from fluksio.flow.demand import demand
node = a_sensor([])
# A worker process binds no controller and cannot see who watches.
assert node.wanted("reading")
reads: set[str] = set()
monkeypatch.setattr(
demand, "_controller", SimpleNamespace(consumed_messages=lambda: reads)
)
assert not node.wanted("reading")
reads.add("demo.reading")
assert node.wanted("reading")
reads.clear()
demand.want("a viewer", {"demo.reading"})
try:
assert node.wanted("reading")
finally:
demand.drop("a viewer")
assert not node.wanted("reading")
+14
View File
@@ -144,6 +144,20 @@ new frames arrive. Use durable storage for recordings.
Local screens receive pushed live frames; portal screens fetch the bytes.
Choose a modest poll rate for remote viewing.
`self.wanted(port)` says whether anyone wants what a port publishes right
now. That is a dashboard tile on screen, a portal screen, or a node of a
running flow reading its message. Check it before fetching a frame so a camera
sends nothing while nobody watches:
```python
async def poll(self):
if not self.wanted("frame"):
return None
...
```
On a remote worker it is always true, since the worker cannot see who watches.
## Keeping state across restarts
Keep connections and device handles in node attributes. Store short strings