Recorded performances: one play of a song as Jev arranged it, stored on the site (worker/src/listening.ts) under a random id, with who played it (the Worker reads the session), and replayed from a link, /songs/<theme>/<song>/?performance=<id>, or for a listener's song /?listener=<id>&performance=<id>, which replays in its sandbox.
- Recording (JevPerformance.jsx's recorder): when a song as written ends,
or is stopped (or replaced) after its opening section,
performanceOfreads what every jev() played from the booth: per jev, in declaration order, per segment, its status, Jev's time, the cycle a late answer played from, and each question's answer (choice, score or noul, with confidence, probabilities, and whether it was sampled, a fallback, forced or absent). With the SHA-256 of the code that played, it is posted once; the booth then offers the link. - Replaying (useReplContext, on a song page with ?performance=): the
performance is fetched before the song is evaluated and handed to
jevCore's
setReplay, so every jev() plays the recorded answers and asks Jev nothing. When the song's code hash differs, it says the song has changed since, and plays the performance where the form still allows (jevCore.mjs, Replay). Loading another song ends it.
21import { setReplay } from './jevCore.mjs';
what the Worker accepts (listening.ts): names, options, and how many
The performance id a page's query names, or null.
A take's replay link: a site song's page, or, for a listener's song ("listener:<id>", which has no page), the home page with ?listener=, where it replays in the sandbox (client.mjs's loadListenerSong).
SHA-256 of the code, in hex: how a replay knows the song changed.
55const unit = (x) => typeof x === 'number' && Number.isFinite(x) && x >= 0 && x <= 1;
One answer as it played, with only what the Worker keeps.
58export function answerOf(a) { 59 const out = {}; 60 if (typeof a.choice === 'string') out.choice = a.choice; 61 else if (typeof a.score === 'number') out.score = a.score; 62 else if (typeof a.noul === 'number') out.noul = a.noul; 63 if (unit(a.confidence) && !a.fallback) out.confidence = a.confidence; 64 const p = Object.entries(a.probabilities ?? {}).filter(([k, v]) => OPTION.test(k) && unit(v)); 65 if (p.length) out.probabilities = Object.fromEntries(p.slice(0, 64)); 66 if (typeof a.sampled === 'number' && a.sampled > 0) out.sampled = a.sampled; 67 if (typeof a.top === 'string' && OPTION.test(a.top)) out.top = a.top; 68 for (const flag of ['fallback', 'forced', 'absent']) if (a[flag]) out[flag] = true; 69 return out; 70}
The performance in booth (boothStore's snapshot) through segment
upTo, or null when there is nothing to record: no jev(), jev()s on
different cadences, or more of them than the site keeps.
75export function performanceOf(booth, { song, codeHash, ended, upTo }) { 76 const views = booth?.jevs ?? (booth ? [booth] : []); 77 if (!views.length || views.length > MAX_JEVS || views.some((v) => v.every !== views[0].every)) return null; 78 const last = Math.min(upTo, MAX_SEGMENTS - 1); 79 let form = null; 80 const jevs = views.map((view, jev) => { 81 if (view.form) form = { jev, question: view.form.name }; 82 const segments = []; 83 for (let n = 0; n <= last; n++) { 84 const d = view.decisions[n]; 85 if (!d) break; 86 // still being asked when the song stopped: it played its fallbacks 87 const status = d.status === 'asking' ? 'fallback' : d.status; 88 const values = Object.fromEntries( 89 Object.entries(d.values) 90 .filter(([name]) => NAME.test(name)) 91 .map(([name, a]) => [name, answerOf(a)]), 92 ); 93 segments.push({ 94 status, 95 ...(status === 'answered' && Number.isFinite(d.ms) ? { ms: Math.round(d.ms) } : {}), 96 ...(Number.isInteger(d.from) && status === 'answered' ? { from: d.from } : {}), 97 values, 98 }); 99 } 100 return { segments }; 101 }); 102 if (jevs.some((j) => !j.segments.length)) return null; 103 return { song, codeHash, model: views[0].model, every: views[0].every, ended, form, jevs }; 104}
Stores a performance; resolves to its id.
107export async function postPerformance(body, fetchImpl = globalThis.fetch) { 108 const res = await fetchImpl(PERFORMANCES, { 109 method: 'POST', 110 headers: { 'Content-Type': 'application/json' }, 111 body: JSON.stringify(body), 112 keepalive: true, 113 }); 114 if (!res.ok) throw new Error(`the site answered ${res.status}: ${(await res.text()).slice(0, 120)}`); 115 const { id } = await res.json(); 116 if (!ID.test(id ?? '')) throw new Error('the site answered without an id'); 117 return id; 118}
120export async function fetchPerformance(id, fetchImpl = globalThis.fetch) { 121 const res = await fetchImpl(`${PERFORMANCES}/${id}`); 122 if (res.status === 404) throw new Error('no such performance'); 123 if (!res.ok) throw new Error(`the site answered ${res.status}`); 124 return res.json(); 125}
Arms the replay of performance id for song (the page's, from the
song index): every jev() evaluated from now on plays it. Resolves to
{ changed } (whether the song's code differs from the one recorded), or
throws when it cannot be replayed, leaving Jev to play the song live.
131export async function armReplay(id, song, fetchImpl = globalThis.fetch) { 132 const performance = await fetchPerformance(id, fetchImpl); 133 if (performance.song !== song.id) throw new Error(`the performance is of ${performance.song}, not ${song.id}`); 134 const changed = performance.codeHash !== (await codeHash(song.code)); 135 setReplay({ jevs: performance.jevs, changed }); 136 return { changed, performance }; 137}
139export const endReplay = () => setReplay(null);
What the booth shows of the latest recording: for which evaluation (its first view), and whether it is being saved, saved (with its link) or failed.
143let recording = null; 144const listeners = new Set(); 145export const recordingStore = { 146 subscribe(listener) { 147 listeners.add(listener); 148 return () => listeners.delete(listener); 149 }, 150 get: () => recording, 151 set(next) { 152 recording = next; 153 for (const listener of listeners) listener(); 154 }, 155};