All checks were successful
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (pull_request) Successful in 32s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 5s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 6s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 5s
WAI Studio / does this change touch it (pull_request) Successful in 5s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 31s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 54s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m21s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 8m19s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 9m24s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 8m56s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m5s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 9m35s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m43s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 11m30s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 3m40s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 19m26s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 6m1s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 6m30s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 7m27s
The room page took a turn's end from its own schedule only. When a lead's new schedule took effect in the middle of a turn, the page of the performer holding it went on sending up to the new schedule's `from`, past its guard (co-performance §8). The relay judged its last objects on arrival, under the schedule they reached it in, and forwarded them. The room's clock, a hop later, logged them after the `from`, under the new schedule. Where the new schedule refuses and gives that turn to someone else, the room with the clock's log then refuses as `turn_violated`. How often depends on that hop: rarely on a quiet host, and once in #619's browser run, on the shared build host under load (the room e2e's refusing schedule takes effect in the middle of the marking schedule's sixth turn, which the other holder held). Now: - A turn ends at its segment's boundary, or at the `from` of a later schedule, if that comes first (§6.1: "A later schedule replaces an earlier one from its `from` on"). - The guard applies before either. - The turns ahead step from the cut turn to the new schedule's first turn. Before, they jumped to the old turn's end and skipped the new schedule's turns up to it. Tests: - `web/room/co.test.mjs` (new, run by `e2e/run.sh` with the page's other module tests): six cases. Against the old `co.js`, three of them fail: the turn's end, the guard before the `from`, and the turns ahead. - The room e2e names an object it refuses: its arrival, and how far it is from the nearest turn boundary or schedule `from`. If the check fails again, it says which race it was. Co-Authored-By: Claude Opus 5.5 <[email protected]>
432 lines
20 KiB
JavaScript
432 lines
20 KiB
JavaScript
// Co-performance in the room page (WAI co-performance): what the room's statements say, shown
|
|
// as they arrive, and what this page sends or asks for.
|
|
//
|
|
// - The floor: the turn schedule in effect (WPT1) and its handovers (WPH1), who holds the floor
|
|
// now and the turns ahead, in microseconds or bars at a tempo; the marks the room's clock
|
|
// signs (WPM1) for an object out of turn or late.
|
|
// - What's next: each participant's intents (WPI1) as upcoming lines, and, once the instant has
|
|
// passed, what played: kept, diverged or missed, read from the participant's own captions and
|
|
// chats under the room's policy (WPP1); an AI participant's marked as an AI system's.
|
|
// - The audience: the selection window open now (WPW1), and, for a viewer whose grant carries
|
|
// the audience-input power, its options or a prompt box; each window's commitment (WPK1) and
|
|
// selection (WPS1) with its credits.
|
|
// - Fidelity: the clock's last fidelity statement (WPF1), each participant's kept of announced.
|
|
//
|
|
// A lead (a grant with the direct power) asks the room's clock to state a schedule, a handover
|
|
// or a window, and a fidelity statement, through the room service; the holder of a turn may
|
|
// hand it over. A performer's page sends media only while it holds the floor, a guard inside
|
|
// each turn, so that what it plays arrives in its turn; `playOutOfTurn` lifts that for a
|
|
// moment, as a pickup that starts before the bar line does.
|
|
//
|
|
// What a page shows is its own reading. The room's verification (co-performance §9, with
|
|
// wai-live-web's `Room.coVerify`) is what decides.
|
|
|
|
const GUARD_US = 150_000; // a performer's media starts this far into its turn and stops this far before its end
|
|
const STATEMENT_KIND = 0x01; // `meta` (live-events §4.2)
|
|
const TYPE_BIT = { prompt: 1, sketch: 2, vote: 4, movement: 8 };
|
|
|
|
export class Co {
|
|
constructor(o) {
|
|
Object.assign(this, o); // wai, log, $, nameOf, isAi, roomNow, me, grantOf, present, emit, emitDetached, api
|
|
this.policy = JSON.parse(this.wai.coPolicy(o.policyBytes));
|
|
this.windowUs = (this.policy.window_ms || 120) * 1000;
|
|
this.schedules = []; // in the order held
|
|
this.handovers = [];
|
|
this.intents = [];
|
|
this.said = {}; // principal -> [{t, text, digest}]
|
|
this.marks = { out_of_turn: 0, late: 0, records: [] };
|
|
this.windows = new Map(); // digest -> window
|
|
this.inputs = new Map(); // digest -> input
|
|
this.fidelity = null;
|
|
this.sent = {}; // window digest -> inputs this page sent
|
|
this.outOfTurnUntil = 0;
|
|
this.lateNext = 0;
|
|
this.statements = 0;
|
|
this.requests = [];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------- the records
|
|
|
|
// A record the room accepted (`v`, room.offer's view) and its bytes.
|
|
onRecord(v, bytes) {
|
|
if (v.kind_name === "chat" || v.kind_name === "caption") {
|
|
(this.said[v.principal] = this.said[v.principal] || []).push({ t: v.t, text: v.payload && v.payload.text, digest: v.digest });
|
|
return;
|
|
}
|
|
if (v.kind_name !== "meta") return;
|
|
const s = JSON.parse(this.wai.coEventStatement(bytes));
|
|
if (!s || !s.statement) return;
|
|
this.statements++;
|
|
const at = { t: v.t, principal: v.principal, digest: v.digest };
|
|
switch (s.statement) {
|
|
case "schedule": this.schedules.push({ ...s, ...at, order: this.statements }); break;
|
|
case "handover": this.onHandover(s, at); break;
|
|
case "intent": this.intents.push({ ...s, ...at }); break;
|
|
case "mark":
|
|
if (s.out_of_turn) this.marks.out_of_turn++;
|
|
if (s.late) this.marks.late++;
|
|
this.marks.records.push({ ...s, t: v.t });
|
|
break;
|
|
case "window": this.windows.set(v.digest, { ...s, ...at, inputs: [], commit: null, selection: null }); break;
|
|
case "input": {
|
|
this.inputs.set(v.digest, { ...s, ...at });
|
|
const w = this.windows.get(s.window);
|
|
if (w) w.inputs.push(v.digest);
|
|
break;
|
|
}
|
|
case "commit": { const w = this.windows.get(s.window); if (w) w.commit = { ...s, ...at }; break; }
|
|
case "selection": { const w = this.windows.get(s.window); if (w) w.selection = { ...s, ...at }; break; }
|
|
case "fidelity": this.fidelity = { ...s, ...at }; break;
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
onHandover(s, at) {
|
|
const turn = this.turnAt(s.at);
|
|
if (!turn || !turn.holders.includes(s.to)) return; // the room's verifier refuses it; nothing to show
|
|
this.handovers.push({ ...s, ...at, schedule: turn.schedule.digest, segment: turn.segment, turn: turn.index, order: this.statements });
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------ the floor
|
|
|
|
// The schedule in effect at `x` (§6.1): of those whose `from` is at or below it, the last
|
|
// stated; its index in the room's order, or -1.
|
|
scheduleIndexAt(x) {
|
|
for (let i = this.schedules.length - 1; i >= 0; i--) if (this.schedules[i].from <= x) return i;
|
|
return -1;
|
|
}
|
|
|
|
scheduleAt(x) {
|
|
const k = this.scheduleIndexAt(x);
|
|
return k < 0 ? null : this.schedules[k];
|
|
}
|
|
|
|
// The turn running at room-clock `x` (§6.2), with its holder at `x` (§6.3). A turn ends at its
|
|
// segment's next boundary, or at the `from` of a later schedule, which replaces its own from
|
|
// then on (§6.1), if that comes first: the guard a performer keeps before a turn's end (§8)
|
|
// keeps it before either, or its last objects reach the relay under one schedule and the room
|
|
// clock, a hop later, under the next.
|
|
turnAt(x) {
|
|
const k = this.scheduleIndexAt(x);
|
|
if (k < 0) return null;
|
|
const s = this.schedules[k];
|
|
const replaced = this.schedules.slice(k + 1).reduce((m, r) => Math.min(m, r.from), Infinity);
|
|
for (let i = 0; i < s.segments.length; i++) {
|
|
const g = s.segments[i];
|
|
const L = g.turn_us;
|
|
if (!L) continue;
|
|
if (g.start <= x && x < g.start + g.turns * L) {
|
|
const n = Math.floor((x - g.start) / L);
|
|
const scheduled = g.holders[n % g.holders.length];
|
|
const h = this.handovers.filter((h) => h.schedule === s.digest && h.segment === i && h.turn === n && h.at <= x).sort((a, b) => a.at - b.at || a.order - b.order).pop();
|
|
return { schedule: s, segment: i, index: n, start: g.start + n * L, end: Math.min(g.start + (n + 1) * L, replaced), scheduled, holder: h ? h.to : scheduled, holders: g.holders, role: g.pairs ? (n % 2 ? "response" : "call") : "solo", mode: s.mode, of: g.turns };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// The turns ahead of `x`, under the schedule in effect at each.
|
|
turnsAhead(x, n) {
|
|
const out = [];
|
|
let y = x;
|
|
for (let i = 0; i < n; i++) {
|
|
const t = this.turnAt(y);
|
|
if (!t) {
|
|
// The next segment or schedule to start, if any.
|
|
const next = this.nextStart(y);
|
|
if (next == null) break;
|
|
y = next;
|
|
continue;
|
|
}
|
|
out.push(t);
|
|
y = t.end;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
nextStart(y) {
|
|
let best = null;
|
|
for (const s of this.schedules) for (const g of s.segments) if (g.start > y && (best == null || g.start < best)) best = g.start;
|
|
return best;
|
|
}
|
|
|
|
// Whether this page sends its media now: unless a schedule governs it and another holds the
|
|
// floor, or the turn is within its guard of its start or end (§6.4).
|
|
sends(x) {
|
|
if (x < this.outOfTurnUntil) return true;
|
|
const t = this.turnAt(x);
|
|
if (!t || !t.holders.includes(this.me().principal)) return true;
|
|
return t.holder === this.me().principal && x >= t.start + GUARD_US && x < t.end - GUARD_US;
|
|
}
|
|
|
|
// A moment out of turn, on purpose: a pickup before the bar line.
|
|
playOutOfTurn(ms) {
|
|
this.outOfTurnUntil = this.roomNow() + ms * 1000;
|
|
}
|
|
|
|
// The due instant the next audio object states is `ms` earlier than its capture: it arrives
|
|
// that much later than its due instant.
|
|
stampNextLate(ms) {
|
|
this.lateNext = ms * 1000;
|
|
}
|
|
|
|
takeLate() {
|
|
const l = this.lateNext;
|
|
this.lateNext = 0;
|
|
return l;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------- intents
|
|
|
|
// What played for an intent so far (§5.4, text intents): the author's chat or caption nearest
|
|
// its instant within the policy's window.
|
|
outcome(i, now) {
|
|
if (i.form !== "text") return i.at + this.windowUs < now ? "announced" : "upcoming";
|
|
const near = (this.said[i.principal] || []).filter((x) => Math.abs(x.t - i.at) <= this.windowUs).sort((a, b) => Math.abs(a.t - i.at) - Math.abs(b.t - i.at))[0];
|
|
if (near) return near.text === i.text ? "kept" : "diverged";
|
|
return now > i.at + this.windowUs ? "missed" : "upcoming";
|
|
}
|
|
|
|
// Announce a line `inMs` ahead, and say `say` (the line itself unless given) at its instant,
|
|
// as a caption a machine made when this page is an AI participant.
|
|
async announce({ text, inMs = 1500, spanMs = 1000, say }) {
|
|
const at = this.roomNow() + inMs * 1000;
|
|
const p = this.wai.coEncode(JSON.stringify({ statement: "intent", at, span: spanMs * 1000, form: "text", text }));
|
|
const view = await this.emit(STATEMENT_KIND, p);
|
|
// Said a little before its instant, so that a timer that fires late still says it within
|
|
// the policy's window of it (§5.4).
|
|
const spoken = new Promise((resolve) => {
|
|
setTimeout(async () => {
|
|
try { resolve(await this.say(say == null ? text : say)); } catch (e) { resolve({ error: String(e) }); }
|
|
}, Math.max(0, (at - this.roomNow()) / 1000 - 20));
|
|
});
|
|
this.spoken = spoken;
|
|
return { at, intent: view.digest, spoken };
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------ audience
|
|
|
|
myGrant() {
|
|
return this.grantOf(this.me().principal);
|
|
}
|
|
|
|
audienceLimit() {
|
|
const g = this.myGrant();
|
|
const a = g && g.attrs.find((x) => x[0] === "audience_input");
|
|
return a ? Number(a[1]) : 0;
|
|
}
|
|
|
|
isLead() {
|
|
const g = this.myGrant();
|
|
return !!g && (g.powers & 2) !== 0;
|
|
}
|
|
|
|
openWindow(now) {
|
|
const open = [...this.windows.values()].filter((w) => w.close > now);
|
|
return open[open.length - 1] || null;
|
|
}
|
|
|
|
// An input to window `window` (§7.3): a vote's `option`, or a prompt's `text`.
|
|
async input({ window, type, option, text }) {
|
|
const p = this.wai.coEncode(JSON.stringify({ statement: "input", window, type, option, text }));
|
|
const view = await this.emit(STATEMENT_KIND, p);
|
|
this.sent[window] = (this.sent[window] || 0) + 1;
|
|
this.render();
|
|
return view;
|
|
}
|
|
|
|
// An input signed as this page's next record and sent, but not kept in its chain: for a test
|
|
// that the relay withholds what §7.3 refuses, which nobody then receives.
|
|
async inputDetached(desc) {
|
|
let p;
|
|
try { p = this.wai.coEncode(JSON.stringify({ statement: "input", ...desc })); } catch {
|
|
// A body that does not read: a vote with a byte too many.
|
|
const w = this.wai.coEncode(JSON.stringify({ statement: "input", window: desc.window, type: "vote", option: 0 }));
|
|
p = new Uint8Array(w.length + 1);
|
|
p.set(w);
|
|
}
|
|
return this.emitDetached(STATEMENT_KIND, p);
|
|
}
|
|
|
|
// ------------------------------------------------------------------- asking the room's clock
|
|
|
|
async ask(path, body) {
|
|
const r = await this.api(path, body || {});
|
|
this.requests.push({ path, status: r.status, body: r.body });
|
|
if (r.status !== 200) this.log(`co: ${path} answered ${r.status} ${JSON.stringify(r.body)}`);
|
|
return r;
|
|
}
|
|
|
|
// Two-or-more holders trading turns of `seconds` (mode `mark` or `refuse`), starting soon.
|
|
startTurns({ seconds = 4, turns = 4, mode = "mark", holders, pairs = false }) {
|
|
const hs = holders || this.present().filter((p) => this.isPublisher(p));
|
|
return this.ask("/co/schedule", { mode, beat_us: 0, beats_per_bar: 0, segments: [{ length: Math.round(seconds * 1e6), turns, pairs, holders: hs }] });
|
|
}
|
|
|
|
passFloor() {
|
|
const t = this.turnAt(this.roomNow() + 400_000);
|
|
if (!t) return Promise.resolve({ status: 0, body: { error: "no turn" } });
|
|
const i = t.holders.indexOf(t.holder);
|
|
return this.ask("/co/handover", { to: t.holders[(i + 1) % t.holders.length] });
|
|
}
|
|
|
|
openVote({ question, options = 3, seconds = 8 }) {
|
|
return this.ask("/co/window", { rule: "plurality", options, k: 1, seconds, question });
|
|
}
|
|
|
|
askPrompts({ question, seconds = 8, k = 1, rule = "draw" }) {
|
|
return this.ask("/co/window", { rule, types: ["prompt"], options: 0, k, seconds, question });
|
|
}
|
|
|
|
stateFidelity() {
|
|
return this.ask("/co/fidelity", {});
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------- the view
|
|
|
|
render() {
|
|
const $ = this.$;
|
|
const sec = $("co");
|
|
if (!sec) return;
|
|
sec.hidden = false;
|
|
const now = this.roomNow();
|
|
const me = this.me();
|
|
const name = (p) => (p === me.principal ? "you" : this.nameOf(p)) + (this.isAi(p) ? " (AI system)" : "");
|
|
const s = (us) => `${(us / 1e6).toFixed(1)} s`;
|
|
// The floor.
|
|
const t = this.turnAt(now);
|
|
const nowEl = $("co-now");
|
|
if (nowEl) {
|
|
nowEl.textContent = t
|
|
? `${name(t.holder)} ${t.holder === me.principal ? "hold" : "holds"} the floor · ${s(t.end - now)} left · turn ${t.index + 1} of ${t.of}${t.role === "solo" ? "" : ` (${t.role})`} · out of turn is ${t.mode === "refuse" ? "refused" : "marked"}`
|
|
: this.schedules.length ? "The floor is open: no turn runs now." : "No turn schedule yet: the floor is open.";
|
|
nowEl.dataset.holder = t ? t.holder : "";
|
|
}
|
|
const turnsEl = $("co-turns");
|
|
if (turnsEl) {
|
|
turnsEl.replaceChildren(...this.turnsAhead(now, 6).map((x) => {
|
|
const li = document.createElement("li");
|
|
li.dataset.holder = x.holder;
|
|
li.textContent = `${x.start <= now ? "now" : `in ${s(x.start - now)}`}: ${name(x.holder)}${x.holder !== x.scheduled ? " (handed over)" : ""}${x.role === "solo" ? "" : `, ${x.role}`}`;
|
|
return li;
|
|
}));
|
|
}
|
|
const marksEl = $("co-marks");
|
|
if (marksEl) marksEl.textContent = this.marks.out_of_turn + this.marks.late ? `Marked by the room's clock: ${this.marks.out_of_turn} out of turn, ${this.marks.late} late.` : "";
|
|
// What's next.
|
|
const intentsEl = $("co-intents");
|
|
if (intentsEl) {
|
|
const shown = this.intents.filter((i) => i.at + i.span > now - 20e6).slice(-8);
|
|
intentsEl.replaceChildren(...shown.map((i) => {
|
|
const li = document.createElement("li");
|
|
const o = this.outcome(i, now);
|
|
li.dataset.outcome = o;
|
|
li.dataset.intent = i.digest;
|
|
const what = i.form === "text" ? `“${i.text}”` : i.form === "object" ? `an object ${i.digest.slice(0, 8)}` : i.form;
|
|
li.textContent = o === "upcoming" ? `${name(i.principal)} · next, in ${s(Math.max(0, i.at - now))}: ${what}` : `${name(i.principal)} · ${what} · ${o}`;
|
|
return li;
|
|
}));
|
|
}
|
|
// The audience.
|
|
const w = this.openWindow(now);
|
|
const q = $("co-question");
|
|
if (q) q.textContent = w ? `${w.question || "The audience is asked"} · closes in ${s(w.close - now)}` : "No window is open.";
|
|
const form = $("co-input");
|
|
const limit = this.audienceLimit();
|
|
const present = this.present().includes(me.principal);
|
|
if (form) {
|
|
const take = w && limit > 0 && present;
|
|
form.hidden = !take;
|
|
if (take && form.dataset.window !== w.digest) {
|
|
form.dataset.window = w.digest;
|
|
const opts = $("co-options");
|
|
opts.replaceChildren();
|
|
if (w.rule === "plurality") {
|
|
for (let o = 0; o < w.options; o++) {
|
|
const b = document.createElement("button");
|
|
b.type = "button";
|
|
b.textContent = `Option ${o + 1}`;
|
|
b.dataset.option = String(o);
|
|
b.addEventListener("click", () => this.input({ window: w.digest, type: "vote", option: o }).catch((e) => this.log(`co: ${e}`)));
|
|
opts.appendChild(b);
|
|
}
|
|
}
|
|
$("co-prompt-row").hidden = w.rule === "plurality";
|
|
}
|
|
const left = w ? limit - (this.sent[w.digest] || 0) : 0;
|
|
if ($("co-left")) $("co-left").textContent = take ? `${left} of ${limit} input${limit === 1 ? "" : "s"} left` : "";
|
|
for (const b of form.querySelectorAll("button")) b.disabled = left <= 0;
|
|
}
|
|
const selEl = $("co-selections");
|
|
if (selEl) {
|
|
const done = [...this.windows.values()].filter((x) => x.selection).slice(-4);
|
|
selEl.replaceChildren(...done.map((x) => {
|
|
const li = document.createElement("li");
|
|
li.dataset.window = x.digest;
|
|
const sel = x.selection;
|
|
const picks = sel.picks.map((p, n) => {
|
|
const c = sel.credits[n];
|
|
if (p.option != null) return `option ${p.option + 1}, credited to its ${c.count} voter${c.count === 1 ? "" : "s"} together`;
|
|
const inp = this.inputs.get(p.input);
|
|
return `“${inp && inp.text != null ? inp.text : p.input.slice(0, 8)}”, credited to ${name(c.party)}`;
|
|
});
|
|
li.textContent = `${x.question || "a window"}: ${picks.length ? picks.join("; ") : "nothing selected"} (${x.commit ? x.commit.count : 0} input${x.commit && x.commit.count === 1 ? "" : "s"} committed, ${sel.rule})`;
|
|
return li;
|
|
}));
|
|
}
|
|
// Fidelity.
|
|
const fEl = $("co-fidelity");
|
|
if (fEl && this.fidelity) {
|
|
fEl.replaceChildren(...this.fidelity.entries.map((e) => {
|
|
const li = document.createElement("li");
|
|
li.dataset.principal = e.principal;
|
|
li.textContent = `${name(e.principal)}: kept ${e.kept} of ${e.announced} announced (${e.diverged} diverged, ${e.missed} missed)`;
|
|
return li;
|
|
}));
|
|
}
|
|
const lead = $("co-lead");
|
|
if (lead) lead.hidden = !this.isLead();
|
|
const pass = $("co-pass");
|
|
if (pass) pass.hidden = !(t && (t.holder === me.principal || this.isLead()));
|
|
}
|
|
|
|
snapshot() {
|
|
const now = this.roomNow();
|
|
return {
|
|
now,
|
|
policy: this.policy,
|
|
schedules: this.schedules.map((s) => ({ digest: s.digest, mode: s.mode, from: s.from, segments: s.segments })),
|
|
handovers: this.handovers.map((h) => ({ digest: h.digest, at: h.at, to: h.to })),
|
|
turn: this.turnAt(now),
|
|
marks: { out_of_turn: this.marks.out_of_turn, late: this.marks.late },
|
|
intents: this.intents.map((i) => ({ digest: i.digest, principal: i.principal, at: i.at, text: i.text, outcome: this.outcome(i, now) })),
|
|
windows: [...this.windows.values()].map((w) => ({ digest: w.digest, rule: w.rule, close: w.close, question: w.question, inputs: w.inputs.length, commit: w.commit && { count: w.commit.count, digest: w.commit.digest }, selection: w.selection && { picks: w.selection.picks, credits: w.selection.credits } })),
|
|
fidelity: this.fidelity && { t: this.fidelity.t, through: this.fidelity.through, entries: this.fidelity.entries, digest: this.fidelity.digest },
|
|
audience: { limit: this.audienceLimit(), sent: this.sent },
|
|
lead: this.isLead(),
|
|
requests: this.requests.map((r) => ({ path: r.path, status: r.status })),
|
|
};
|
|
}
|
|
}
|
|
|
|
// The wiring of the section's controls (index.html), once the page has a Co.
|
|
export function wireControls(co, $, fail) {
|
|
const on = (id, f) => { const el = $(id); if (el) el.addEventListener("click", () => f().catch(fail)); };
|
|
const num = (id, d) => Number(($(id) || {}).value || d);
|
|
on("co-start", () => co.startTurns({ seconds: num("co-turn-s", 4), turns: num("co-turns-n", 4), mode: ($("co-mode") || {}).value || "mark" }));
|
|
on("co-pass", () => co.passFloor());
|
|
on("co-vote", () => co.openVote({ question: ($("co-q") || {}).value || "Which way next?", options: num("co-options-n", 3), seconds: num("co-window-s", 8) }));
|
|
on("co-prompts", () => co.askPrompts({ question: ($("co-q") || {}).value || "What should happen next?", seconds: num("co-window-s", 8) }));
|
|
on("co-fid", () => co.stateFidelity());
|
|
const form = $("co-input");
|
|
if (form) form.addEventListener("submit", (e) => {
|
|
e.preventDefault();
|
|
const el = $("co-prompt");
|
|
const text = el.value.trim();
|
|
if (!text || !form.dataset.window) return;
|
|
el.value = "";
|
|
co.input({ window: form.dataset.window, type: "prompt", text }).catch(fail);
|
|
});
|
|
}
|
|
|
|
export { GUARD_US, TYPE_BIT };
|