What the page shows of Jev at work: the booth and mixer under the editor,
the highlighted choices in the code, and the request log all read this
store, and each jev({...}).every(...) (jevCore.mjs) writes to it.
A song may declare several jev()s (one choosing the section, one the parts, asked after it). The page shows them as one: a cell per section with every answer from every jev on that cadence.
Every evaluation builds new views. They are drafts until the evaluation succeeds (afterEval), so code that fails to evaluate leaves the views of the song still playing on screen, and code without jev() clears them.
A listener's song plays in a sandbox (sandbox.mjs), whose booth arrives as data: shown instead of the page's own until the page evaluates again.
20const STATUS_RANK = { asking: 3, fallback: 2, answered: 1, opening: 0 };
Several views on one cadence, seen as one.
23export function combine(views) { 24 if (!views?.length) return null; 25 if (views.length === 1) return views[0]; 26 const every = views[0].every; 27 const same = views.filter((v) => v.every === every); 28 const lasts = same.map((v) => v.last).filter((l) => l !== null); 29 const length = Math.max(...same.map((v) => v.decisions.length)); 30 const decisions = []; 31 for (let n = 0; n < length; n++) { 32 const parts = same.map((v) => v.decisions[n]).filter(Boolean); 33 if (!parts.length) continue; 34 const status = parts.map((d) => d.status).sort((a, b) => STATUS_RANK[b] - STATUS_RANK[a])[0]; 35 decisions[n] = { 36 segment: n, 37 cycles: parts[0].cycles, 38 status, 39 values: Object.assign({}, ...parts.map((d) => d.values)), 40 problems: parts.flatMap((d) => d.problems ?? []), 41 reason: parts.find((d) => d.reason)?.reason, 42 // the song ended before this segment: nothing was needed, nothing failed 43 ended: parts.some((d) => d.ended), 44 ms: parts.some((d) => d.ms !== undefined) ? Math.max(...parts.map((d) => d.ms ?? 0)) : undefined, 45 states: parts.map((d) => d.state).filter(Boolean), 46 }; 47 } 48 return { 49 model: views[0].model, 50 about: views[0].about, 51 every, 52 segments: Math.min(...same.map((v) => v.segments)), 53 questions: same.flatMap((v) => v.questions), 54 form: same.find((v) => v.form)?.form ?? null, 55 last: lasts.length ? Math.min(...lasts) : null, 56 decisions, 57 reactions: views[0].reactions, 58 replay: views[0].replay ?? null, 59 // every jev hears the listener 60 react: (segment, kind) => same.forEach((v) => v.react(segment, kind)), 61 views: same, 62 // every jev() of the evaluation, in declaration order (a performance 63 // records them all; a song's are all on one cadence) 64 jevs: views, 65 }; 66}
68function emit() { 69 snapshot = { booth: remote ?? combine(current), version: snapshot.version + 1 }; 70 for (const listener of listeners) listener(); 71} 72 73export const boothStore = { 74 subscribe(listener) { 75 listeners.add(listener); 76 return () => listeners.delete(listener); 77 }, 78 getSnapshot: () => snapshot, 79 // jevCore.mjs, as each jev().every() is declared 80 // Returns the view's index: its jev()'s place in the song, in declaration 81 // order, which is how a recorded performance names it (performance.mjs). 82 adopt(view) { 83 draft.push(view); 84 return draft.length - 1; 85 }, 86 changed(view) { 87 if (current?.includes(view)) emit(); 88 }, 89 // The views of the song about to start: the evaluation's drafts before 90 // it is committed (the scheduler starts inside evaluate), else the 91 // committed ones (play pressed later). 92 starting: () => (draft.length ? draft : (current ?? [])), 93 // useReplContext: around each evaluation. 94 beginEval() { 95 draft = []; 96 }, 97 // A block evaluation (`range` set) replaces only its block, so one 98 // without jev() keeps the views that are playing. 99 commitEval({ block = false } = {}) { 100 if (block && !draft.length) return; 101 current = draft.length ? draft : null; 102 draft = []; 103 remote = null; 104 emit(); 105 }, 106 // sandbox.mjs: the sandbox's booth, already checked (sandboxProtocol.mjs), 107 // or null when the sandbox is gone. 108 showRemote(booth) { 109 remote = booth; 110 if (booth) current = null; 111 emit(); 112 }, 113};
The section playing at cycle, or null: its segment, decision, how far
into it and how many cycles are left (the booth's strip, and the MCP's
react in tabTools.mjs).
118export function playing(booth, cycle) { 119 if (!booth || cycle === null || cycle < 0) return null; 120 const segment = Math.floor(cycle / booth.every); 121 if (segment >= booth.segments || (booth.last !== null && segment > booth.last)) return null; 122 const d = booth.decisions[segment]; 123 if (!d) return null; 124 return { 125 segment, 126 decision: d, 127 progress: (cycle - segment * booth.every) / booth.every, 128 left: (segment + 1) * booth.every - cycle, 129 }; 130}