Files
app/backend/fluksio/flow/demand.py
T
Melvin StroblandClaude Opus 5.5 62c0b915f3 Cameras on devices (openpanel C1): frames in memory, a binary uplink, demand
- Volatile frames are held in memory with a byte-bounded queue instead of
  the /dev/shm ring scanned on every put; workers keep writing to the
  directory, which is trimmed on a timer. A volatile upload no longer spools
  to disk. ARTIFACT_VOLATILE_BYTES=0 turns the ring off.
- A device sends camera frames as binary messages on its socket, held and
  published in one step and credited to its device node; refused frames are
  logged and dropped. Its snapshot carries demand and the uplink (binary, or
  http through the portal).
- One demand registry for every socket's wanted media, the tunnel's
  included, plus what running flows read, mapped onto on-demand ports and
  published as device_demand; at most 8 media names per socket.
- Per-message deflate off in serve and dev; a device may fetch the last 8
  digests of a message; published dashboards cached per store revision; a
  revoked device socket is closed with 1008.
- Media tiles want bytes only while on screen; the Devices tab previews a
  wired camera; the device node warns when a flow reads an on-demand port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-16 20:43:14 +02:00

115 lines
4.0 KiB
Python

"""Which of a device's ports someone wants right now.
A camera is sent nothing while nobody watches it. A port the device reports as
``on_demand`` has demand while a viewer socket wants bytes for the message it
is wired to — a dashboard tile, the Devices tab's preview, a screen through the
portal — or a node of a running flow reads that message.
Every socket says what it wants here, in one registry, and the result is
published on the bus as a ``device_demand`` event whenever a device's set
changes. Each device socket picks out its own and tells the device, so nothing
here has to know which sockets belong to which device.
"""
from __future__ import annotations
import threading
import time
from collections.abc import Hashable
from typing import Any
from fluksio.flow.events import event_bus
class Demand:
"""The media names every live socket wants, and what that asks of devices.
One per process, as the sockets are. ``bind`` hands it the controller,
whose device store says what is wired and whose graph says what a flow
reads.
"""
def __init__(self) -> None:
self._lock = threading.Lock()
self._wanted: dict[Hashable, frozenset[str]] = {}
#: The last set published per device, so only a change is.
self._sent: dict[str, tuple[str, ...]] = {}
self._refresh_lock = threading.Lock()
self._controller: Any = None
def bind(self, controller: Any) -> None:
self._controller = controller
self.refresh()
def want(self, viewer: Hashable, names: set[str]) -> None:
"""What one socket wants bytes for now, replacing what it said before."""
with self._lock:
if names:
self._wanted[viewer] = frozenset(names)
else:
self._wanted.pop(viewer, None)
self.refresh()
def drop(self, viewer: Hashable) -> None:
"""A socket closed, and wants nothing any more."""
self.want(viewer, set())
def viewed(self) -> set[str]:
with self._lock:
return set().union(*self._wanted.values())
def ports(self, device_id: str) -> list[str]:
"""The device's ports that have demand, sorted."""
return list(self._compute().get(device_id, ()))
def refresh(self) -> None:
"""Work demand out again and publish what changed.
Called whenever its inputs move: a socket's wanted set, and a rebuild,
a started or stopped flow or a new report, which change what is wired
and what reads it.
"""
with self._refresh_lock:
found = self._compute()
for device_id in sorted(set(found) | set(self._sent)):
ports = found.get(device_id, ())
if ports == self._sent.get(device_id, ()):
continue
if ports:
self._sent[device_id] = ports
else:
self._sent.pop(device_id, None)
event_bus.publish(
{
"type": "device_demand",
"device": device_id,
"ports": list(ports),
"ts": time.time(),
}
)
def _compute(self) -> dict[str, tuple[str, ...]]:
controller = self._controller
devices = getattr(controller, "devices", None)
if devices is None:
return {}
wanted = self.viewed() | controller.consumed_messages()
found: dict[str, tuple[str, ...]] = {}
for device_id in devices.wiring():
reported = devices.reported_ports(device_id) or {}
ports = tuple(
sorted(
binding.port
for binding in devices.bound(device_id)
if binding.direction == "in"
and binding.message in wanted
and reported.get(binding.port, {}).get("on_demand") is True
)
)
if ports:
found[device_id] = ports
return found
demand = Demand()