open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/binding.js
Transaction Science b73d691ed4 wai: the energy order of dispatch, and the energy budget that lifts a learned capability
SPEC §4, §7.1 step 2: a sink tries an envelope's capabilities in the energy
order unless its deployer configures another: a decode the platform answers
supported and power-efficient first, a learned capability (NeuralIntegerExact,
NeuralEntropyExact, NeuralFloat, Behavioral) with no energy budget after every
other, the envelope's order within each. The order changes which capability
decodes, never a reason. An energy budget is a decode figure measured on a
named reference sink under OnChipCounter or CalibratedInstrument, in a record
named by its digest; no registration states one yet (REGISTRY, Energy budgets;
the export's optional energy_budget, schema only).

wai-rs: energy_order (DispatchPolicy, Budgets, EnergyBudget, is_learned);
Wai::dispatch_verified and WaiMulti::pick apply it, dispatch_with and pick_with
take another order, the HTTP client's default order and the C ABI's
wai_envelope_select follow it. Tests that are about pins state the manifest
order they run in.

browser-sink §4.7: objects sharing a choice are alternatives; the page decodes
the first in the energy order that routes (no_alternative otherwise).
browser_sink::choose_report, the page's chooseReport (wai-live-web, binding.js),
a choices section in live-conformance/browser (existing sections unchanged),
checked by wai-rs, the page's corpus run and browser_sink_verify.py (34 of 34
mutations caught).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 11b2556827)
2026-10-26 08:30:49 -04:00

283 lines
13 KiB
JavaScript

// The browser sink in a page (WAI extensions/browser-sink.md): the capability report a page
// builds and the route of each object (§3, §4), the content binding every decoded frame
// carries (§7) in the side table a page keeps until a registry lists the member (§9.2), and
// the state of the frame on screen (§10).
//
// What is normative is computed by wai-live-web's wasm, the same Rust the corpus
// live-conformance/browser holds: `routeReport`, `chooseReport`, `makeBinding`, `frameTier`, `frameState`.
// This module asks the user agent what the report needs, keeps the side table over live
// decoders and frames, and draws the state behind a debug control.
export const ALG = "jwp-object-1";
// ------------------------------------------------------------------ §9.2: the side table
// Every frame's binding, by frame object, in this realm. Weak: an entry lives as long as
// the frame object does. Shared by every table, so that `clone()` (patched below) carries a
// binding whichever table attached it.
const frameEntries = new WeakMap();
const STRUCK = Symbol("struck");
// The pending entries of each decoder, the refusals and the count struck, for a page's
// decoders; `of(frame)` for any frame. An entry is `{ binding, ... }`, `binding` the
// canonical form (or a function of the frame that returns it, for a tier that depends on
// the frame's format, §7.2), and anything else the page wants back with it.
export class SideTable {
constructor(bound = 64) {
this.bound = bound;
this.pending = new Map(); // decoder -> [[timestamp, entry | STRUCK], ...], in entry order
this.refusals = [];
this.struck = 0;
this.ops = 0;
this.bound_frames = 0;
this.unbound_frames = 0;
}
// Step 1: before a decoder is given a chunk.
chunk(decoder, timestamp, entry) {
const at = this.ops++;
let q = this.pending.get(decoder);
if (!q) { q = []; this.pending.set(decoder, q); }
const hit = q.find((e) => e[0] === timestamp);
if (hit) {
if (hit[1] !== STRUCK) this.refusals.push({ at, reason: "binding_ambiguous" });
hit[1] = STRUCK;
return;
}
q.push([timestamp, entry || null]);
if (q.length > this.bound) { q.shift(); this.struck++; }
}
// Step 2: a decoder's output. Returns the entry attached, or null.
output(decoder, frame) {
this.ops++;
const q = this.pending.get(decoder) || [];
const i = q.findIndex((e) => e[0] === frame.timestamp);
let entry = null;
if (i >= 0) {
const e = q.splice(i, 1)[0][1];
entry = e === STRUCK ? null : e;
}
if (entry && typeof entry.binding === "function") entry = { ...entry, binding: entry.binding(frame) };
if (entry && entry.binding) { frameEntries.set(frame, entry); this.bound_frames++; } else { frameEntries.delete(frame); this.unbound_frames++; entry = null; }
return entry;
}
// A frame a wasm decoder output with its binding (§7.2): attached directly.
attach(frame, entry) {
if (entry && entry.binding) frameEntries.set(frame, entry); else frameEntries.delete(frame);
}
// Step 3: reset(), close() or an error; and flush() once it resolves.
reset(decoder) {
this.ops++;
const q = this.pending.get(decoder);
this.pending.delete(decoder);
if (q) this.struck += q.length;
}
static of(frame) {
return frameEntries.get(frame) || null;
}
of(frame) {
return SideTable.of(frame);
}
// Step 4, for a frame object that is not a VideoFrame (the corpus's sequences): the
// source's entry attached to the clone.
cloneOnto(src, dst) {
const e = frameEntries.get(src);
if (e) frameEntries.set(dst, e); else frameEntries.delete(dst);
}
// Step 5: what a frame posted to another realm is posted with; the source keeps none.
transferOut(frame) {
const e = frameEntries.get(frame) || null;
frameEntries.delete(frame);
return e;
}
transferIn(frame, entry) {
this.attach(frame, entry);
}
// Step 6.
close(frame) {
frameEntries.delete(frame);
}
}
// Step 4 and 6 for VideoFrame itself: clone() attaches the source's binding to the clone,
// and close() detaches it. Installed once per realm.
export function installFramePatches() {
if (typeof VideoFrame === "undefined" || VideoFrame.prototype.clone.__waiBinding) return;
const clone = VideoFrame.prototype.clone;
const close = VideoFrame.prototype.close;
const c = function () {
const out = clone.call(this);
const e = frameEntries.get(this);
if (e) frameEntries.set(out, e);
return out;
};
c.__waiBinding = true;
VideoFrame.prototype.clone = c;
VideoFrame.prototype.close = function () {
frameEntries.delete(this);
return close.call(this);
};
}
// The corpus's side-table sequences (live-conformance/browser `tables`), run through a
// SideTable over stand-in objects: the result in the corpus's form.
export function runSequence(bound, ops, canonicalOf) {
const t = new SideTable(bound);
const decoders = new Map();
const frames = new Map();
const dec = (n) => { if (!decoders.has(n)) decoders.set(n, { name: n }); return decoders.get(n); };
for (const [i, op] of ops.entries()) {
t.ops = i; // a refusal names the index of its operation in the sequence
switch (op.op) {
case "chunk": t.chunk(dec(op.decoder), op.timestamp, op.binding ? { binding: canonicalOf(op.binding) } : null); break;
case "output": { const f = { name: op.frame, timestamp: op.timestamp }; t.output(dec(op.decoder), f); frames.set(op.frame, f); break; }
case "clone": { const src = frames.get(op.from); if (!src) throw new Error(`clone of ${op.from}, not open`); const f = { name: op.frame }; t.cloneOnto(src, f); frames.set(op.frame, f); break; }
case "transfer": { const src = frames.get(op.from); if (!src) throw new Error(`transfer of ${op.from}, not open`); const e = t.transferOut(src); frames.delete(op.from); const f = { name: op.frame }; t.transferIn(f, op.with_binding === true ? e : null); frames.set(op.frame, f); break; }
case "close": { const f = frames.get(op.frame); if (f) t.close(f); frames.delete(op.frame); break; }
case "reset": case "flush": t.reset(dec(op.decoder)); break;
default: throw new Error(`op ${op.op}`);
}
}
const out = {};
for (const [n, f] of frames) { const e = t.of(f); out[n] = e ? e.binding : null; }
return { frames: out, refusals: t.refusals, struck: t.struck };
}
// ------------------------------------------------------------------ §3, §4: the report
// What the user agent answers for one object: isConfigSupported for the configuration the
// object's parameters give, then, for a supported one only (http-delivery §12.2), Media
// Capabilities' smooth and powerEfficient. No probe runs in this revision (§4.4).
export async function askUserAgent(o, log = () => {}) {
const p = o.params || {};
const video = o.capability.startsWith("wai.video");
const width = p["wai:frame_width"] || 640, height = p["wai:frame_height"] || 360;
const cfg = video
? { codec: o.codec, codedWidth: width, codedHeight: height }
: { codec: o.codec, sampleRate: p["wai:sample_rate"] || 48000, numberOfChannels: p["wai:channel_count"] || 1 };
let supported = false;
try {
const r = await (video ? VideoDecoder : AudioDecoder).isConfigSupported(cfg);
supported = !!r.supported;
} catch (e) {
log(`isConfigSupported(${o.codec}): ${e.message || e}`);
}
let smooth = null, power_efficient = null;
if (supported && typeof navigator !== "undefined" && navigator.mediaCapabilities) {
const q = video
? { type: "file", video: { contentType: `video/mp4; codecs="${o.codec}"`, width, height, bitrate: p["wai:bitrate"] || 1000000, framerate: (p["wai:frame_rate_milli"] || 30000) / 1000 } }
: { type: "file", audio: { contentType: `audio/mp4; codecs="${o.codec}"`, channels: String(p["wai:channel_count"] || 1), bitrate: p["wai:bitrate"] || 64000, samplerate: p["wai:sample_rate"] || 48000 } };
try {
const mc = await navigator.mediaCapabilities.decodingInfo(q);
smooth = !!mc.smooth;
power_efficient = !!mc.powerEfficient;
} catch (e) {
log(`decodingInfo(${o.codec}): ${e.message || e}`);
}
}
return { supported, smooth, power_efficient, probe: "not_run" };
}
// §4.6: the report in two passes. The objects alone give each entry's `asked`; exactly the
// codec strings asked are put to the user agent; then the routes. `ask` is the question
// (askUserAgent by default); every codec string it was called with is returned.
export async function buildReport(wai, objects, { host = {}, wasm = [], constraints = {}, energy = {}, ask = askUserAgent } = {}) {
const report = { browser_sink: 1, ua: {}, host, wasm, constraints, energy, objects };
const plan = JSON.parse(wai.routeReport(JSON.stringify(report)));
const asked = [];
if (plan.outcome === "routed") {
for (const e of plan.table) {
if (!e.asked) continue;
const o = objects.find((x) => x.name === e.name);
if (o.codec in report.ua) continue;
asked.push(o.codec);
report.ua[o.codec] = await ask(o);
}
}
const routed = JSON.parse(wai.routeReport(JSON.stringify(report)));
// §4.7: where objects are alternatives of one content (a shared `choice`), the one to
// decode, in SPEC §4's energy order: the user agent's power-efficient answer first, a
// learned capability with no energy budget last.
const chosen = routed.outcome === "routed" ? JSON.parse(wai.chooseReport(JSON.stringify(report))) : null;
return { report, asked, outcome: routed.outcome, reason: routed.reason || null, table: routed.table || [], choices: chosen && chosen.outcome === "chosen" ? chosen.choices : [] };
}
// ------------------------------------------------------------------ §10: the frame on screen
// A group's admission to its chain, as a page's verifier reports it, in §10's tokens.
export function chainToken(s) {
if (s == null) return "held";
if (s === "linked" || s === "already") return "linked";
if (s === "anchor" || s === "history") return s;
if (s.startsWith("refused")) return "refused";
return "held";
}
export function stateOf(wai, entry, chain, provenance = "not_applicable") {
return JSON.parse(wai.frameState(entry ? entry.binding : "null", "verified", chain, provenance));
}
// The debug view: one row a tile, as §10 lists what it shows.
export function renderStates(el, rows) {
if (!el) return;
const tbody = el.querySelector("tbody");
if (!tbody) return;
tbody.textContent = "";
for (const r of rows) {
const tr = document.createElement("tr");
tr.dataset.state = r.state;
tr.dataset.principal = r.principal;
const pe = r.energy ? r.energy.power_efficient : null;
const fig = r.energy && r.energy.figure ? `${r.energy.figure.per_decode_micro} µJ/decode (${r.energy.figure.class}, ${r.energy.figure.coverage})` : "unmetered";
for (const v of [r.name, r.state, r.tier || "", r.digest ? r.digest.slice(0, 12) : "", r.receipt ? r.receipt.slice(0, 12) : "", r.chain, r.provenance, r.route, pe == null ? "not stated" : String(pe), fig]) {
const td = document.createElement("td");
td.textContent = v;
tr.appendChild(td);
}
tbody.appendChild(tr);
}
el.hidden = false;
}
// §9.1: whether this user agent keeps a `contentBinding` member it does not register, and
// a registered one, in `init.metadata`; through the buffer constructor, the frame
// constructor and clone(). What a test reports, not something a page relies on.
export function metadataProbe() {
const binding = { alg: ALG, digest: "00".repeat(32), tier: "none" };
const out = { buffer: null, frame: null, clone: null, registered: null, errors: [] };
if (typeof VideoFrame === "undefined") return { ...out, errors: ["no VideoFrame"] };
let f = null;
try {
f = new VideoFrame(new Uint8Array(2 * 2 * 4), { format: "RGBX", codedWidth: 2, codedHeight: 2, timestamp: 0, metadata: { contentBinding: binding, rtpTimestamp: 7 } });
const md = f.metadata();
out.buffer = { keys: Object.keys(md), kept: "contentBinding" in md };
out.registered = { rtpTimestamp: md.rtpTimestamp === 7 };
} catch (e) {
out.errors.push(`buffer constructor: ${e.message || e}`);
}
if (f) {
try {
const g = new VideoFrame(f, { metadata: { contentBinding: binding } });
const md = g.metadata();
out.frame = { keys: Object.keys(md), kept: "contentBinding" in md };
const c = g.clone();
out.clone = { keys: Object.keys(c.metadata()), kept: "contentBinding" in c.metadata() };
c.close();
g.close();
} catch (e) {
out.errors.push(`frame constructor: ${e.message || e}`);
}
f.close();
}
out.member_kept = !!((out.buffer && out.buffer.kept) || (out.frame && out.frame.kept));
return out;
}