/** * Media references, and the bytes behind them. * * A frame never travels as a message — the reference does. What the bytes cost * to get is what limits the rate: fetched, it is a round trip per frame and a * camera is a glance; pushed down the socket that already carries the value, * it is a view. Both paths are here, and a caller does not choose between * them: the hook takes whichever arrived. */ import { useEffect, useState } from "react" import { OpenAPI } from "@/client" import { liveStore, useLiveBytes } from "@/components/Flow/liveStore" import { apiToken } from "@/lib/portal" import { si } from "@/lib/utils" /** An artifact reference, as a message carries one. */ export type MediaRef = { digest?: string media_type?: string name?: string size?: number } export const isRef = (value: unknown): value is MediaRef => typeof value === "object" && value !== null && typeof (value as MediaRef).digest === "string" && (value as MediaRef).digest!.startsWith("sha256:") /** * What a reference *is*, in the space a value would have taken: what kind of * bytes, what they were called, how many of them. */ export function describeArtifact(record: Record): string { const name = typeof record.name === "string" && record.name ? record.name : "" const size = typeof record.size === "number" ? `${si(record.size)}B` : "" // The media type when there is one: what kind of bytes these are is the // first thing worth knowing about a frame or a clip, and it is what the // port's own type was declared against. const media = typeof record.media_type === "string" && record.media_type && record.media_type !== "application/octet-stream" ? record.media_type : "artifact" return [media, name, size].filter(Boolean).join(" · ") } /** The header a pushed frame carries in front of its bytes. */ export type MediaFrame = { name: string digest: string media_type: string ts?: number } /** * Split a binary socket frame into what it says and what it carries. * * Four bytes of header length, the header as JSON, then the bytes. Returns * null for anything that does not parse, since a frame the client cannot read * is one to drop rather than one to crash on. */ export function parseMediaFrame( buffer: ArrayBuffer, ): { header: MediaFrame; bytes: Blob } | null { if (buffer.byteLength < 4) return null const length = new DataView(buffer).getUint32(0) if (length <= 0 || 4 + length > buffer.byteLength) return null try { const header = JSON.parse( new TextDecoder().decode(new Uint8Array(buffer, 4, length)), ) as MediaFrame if ( typeof header?.digest !== "string" || typeof header?.name !== "string" ) { return null } return { header, bytes: new Blob([new Uint8Array(buffer, 4 + length)], { type: header.media_type || "application/octet-stream", }), } } catch { return null } } /** * A local URL for an artifact's bytes, refreshed whenever the digest changes. * * Pushed bytes are used where the socket sent them, which is the fast path and * costs no request at all. Otherwise they are fetched: `/artifacts/{digest}` * takes a bearer token and no `` or `