1// What the page shows of Jev at work: the booth and mixer under the editor, 2// the highlighted choices in the code, and the request log all read this 3// store, and each `jev({...}).every(...)` (jevCore.mjs) writes to it. 4// 5// A song may declare several jev()s (one choosing the section, one the 6// parts, asked after it). The page shows them as one: a cell per section 7// with every answer from every jev on that cadence. 8// 9// Every evaluation builds new views. They are drafts until the evaluation 10// succeeds (afterEval), so code that fails to evaluate leaves the views of 11// the song still playing on screen, and code without jev() clears them. 12let draft = []; 13let current = null; 14// A listener's song plays in a sandbox (sandbox.mjs), whose booth arrives 15// as data: shown instead of the page's own until the page evaluates again. 16let remote = null; 17let snapshot = { booth: null, version: 0 }; 18const listeners = new Set(); 19 20const STATUS_RANK = { asking: 3, fallback: 2, answered: 1, opening: 0 }; 21 22// 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} 67 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}; 114 115// The section playing at `cycle`, or null: its segment, decision, how far 116// into it and how many cycles are left (the booth's strip, and the MCP's 117// 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}