jevstrudel.git / website / src / jev / boothStore.mjs

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.

12let draft = [];
13let current = null;

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.

16let remote = null;
17let snapshot = { booth: null, version: 0 };
18const listeners = new Set();
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}