Every listener's 🔥/😴, across plays (worker/src/listening.ts). The
booth's buttons tell Jev how a section lands within one play (jevCore's
audience); each press is also sent here, anonymously, and counted per
(song, section, reaction). The tally comes back:
- on the song's page (the jev panel's song view), per section;
- to Jev, as
listenersSoFarin each request (jev.mjs hands jevCoretallyFor(nowPlaying); jevCore cuts it to the sections that matter).
The playing song's tally is fetched whenever the song changes, and a press is added to it at once, so the page need not ask again.
12import { nowPlaying } from './client.mjs';
14export const REACTIONS = '/jev/reactions';
The name a reaction goes under: the form's section in segment (walk()),
or, for a song without a form, the section's number from 1.
song id → { [section]: { fire, sleep } }
34export const tallyStore = { 35 subscribe(listener) { 36 listeners.add(listener); 37 return () => listeners.delete(listener); 38 }, 39 // changes whenever any song's tally does 40 version: () => version, 41}; 42 43export const tallyFor = (song) => (song ? (tallies.get(song) ?? null) : null); 44 45export async function loadTally(song, fetchImpl = globalThis.fetch) { 46 if (!song || loading.has(song)) return; 47 loading.add(song); 48 try { 49 const res = await fetchImpl(`${REACTIONS}?song=${encodeURIComponent(song)}`); 50 if (!res.ok) return; 51 const { sections } = await res.json(); 52 if (!Array.isArray(sections)) return; 53 tallies.set( 54 song, 55 Object.fromEntries(sections.map(({ section, fire, sleep }) => [String(section), { fire: fire | 0, sleep: sleep | 0 }])), 56 ); 57 changed(); 58 } catch { 59 // no tally: the page shows none and Jev hears none 60 } finally { 61 loading.delete(song); 62 } 63}
Counts one reaction on the site, fire and forget, and in this page's copy of the tally straight away.
67export function sendReaction({ song, section, reaction }, fetchImpl = globalThis.fetch) { 68 if (!song || !section) return; 69 const tally = tallies.get(song) ?? {}; 70 const row = (tally[section] ??= { fire: 0, sleep: 0 }); 71 row[reaction] += 1; 72 tallies.set(song, tally); 73 changed(); 74 try { 75 fetchImpl?.(REACTIONS, { 76 method: 'POST', 77 headers: { 'Content-Type': 'application/json' }, 78 body: JSON.stringify({ song, section, reaction }), 79 keepalive: true, 80 })?.catch?.(() => {}); 81 } catch { 82 // not counted 83 } 84}