Files
app/frontend/src/lib/media.check.ts
T
stroblme 40864d5ca5 Push a frame instead of storing and fetching it
The rate the media dtypes could carry was one frame every second or two: each
was a file on the data volume, an event on the socket, and a request back for
the bytes. This closes both halves of that, and they are one feature.

`save_artifact(..., volatile=True)` writes to a `VolatileStore` — the same
content-addressed store, in `/dev/shm`, bounded by size with the oldest falling
out (`ARTIFACT_VOLATILE_BYTES`, 48 MB under the container's raised `shm_size`).
Nothing sweeps it: a frame nobody kept is not worth walking the store to find.
`ArtifactStore.path` falls through to it, which is what lets a volatile frame be
an ordinary reference everywhere else — the dtype check, a panel's digest scope,
`load_artifact` in a node, and the widget's own fetch all work on one unchanged.
`adopt` copies one into the store when a run records it, so "returned media is
kept, emitted media is not" stays true.

The bytes then go down the flows websocket as a length-prefixed binary frame,
sent just ahead of the `message_value` naming them, so a tile has the frame when
it hears the value moved. Nothing is pushed unasked: a client names the messages
it is drawing (`{"type":"media","names":[…]}`), a panel's list is intersected
with the scope it already had, and only the newest frame per name in a batch is
sent — a client that fell behind is not handed frames it would draw over. The
tunnel relays text only, so a screen reached through a portal falls back to
fetching, which is why the rate table now has two rows.

Around the edges: the remote worker's fetch cache is bounded at last
(`FLUKSIO_ARTIFACT_CACHE_BYTES`), since content addressing means nothing in it
ever expires and a media stream fills it with chunks nothing asks for twice; a
port carrying an image draws the frame in the node panel rather than only
saying `image/png · frame.png · 1.79kB`; and an edge chip says that much instead
of a line of hash. The media screenshot stops waiting for `networkidle` — a
camera is a socket that never goes quiet, which is the point of it.
2026-09-02 10:15:14 +02:00

64 lines
2.0 KiB
TypeScript

/**
* The framing a pushed frame arrives in, and what a malformed one must do.
*
* Run: `bun src/lib/media.check.ts` (there is no unit runner; the suite in
* `tests/` drives a running stack).
*/
import assert from "node:assert/strict"
import { describeArtifact, isRef, parseMediaFrame } from "./media"
function frame(header: object, payload: Uint8Array): ArrayBuffer {
const encoded = new TextEncoder().encode(JSON.stringify(header))
const out = new Uint8Array(4 + encoded.length + payload.length)
new DataView(out.buffer).setUint32(0, encoded.length)
out.set(encoded, 4)
out.set(payload, 4 + encoded.length)
return out.buffer
}
const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
const parsed = parseMediaFrame(
frame(
{
type: "media",
name: "cam.frame",
digest: `sha256:${"a".repeat(64)}`,
media_type: "image/png",
ts: 1,
},
bytes,
),
)
assert.ok(parsed)
assert.equal(parsed.header.name, "cam.frame")
assert.equal(parsed.header.media_type, "image/png")
assert.equal(parsed.bytes.size, 4)
assert.equal(parsed.bytes.type, "image/png")
// A frame the client cannot read is dropped, never thrown on: this runs on
// every socket message.
assert.equal(parseMediaFrame(new ArrayBuffer(0)), null)
assert.equal(parseMediaFrame(new ArrayBuffer(2)), null)
assert.equal(parseMediaFrame(frame({ name: "n" }, bytes)), null)
// A length longer than the frame is truncation, not a payload.
const short = new Uint8Array(8)
new DataView(short.buffer).setUint32(0, 999)
assert.equal(parseMediaFrame(short.buffer), null)
assert.ok(isRef({ digest: `sha256:${"b".repeat(64)}` }))
assert.equal(isRef({ digest: "nope" }), false)
assert.equal(isRef(null), false)
assert.equal(
describeArtifact({ media_type: "image/png", name: "f.png", size: 48000 }),
"image/png · f.png · 48kB",
)
// No media type is still worth summarising as something.
assert.equal(
describeArtifact({ media_type: "application/octet-stream", size: 12 }),
"artifact · 12B",
)
console.log("media.check.ts ok")