"Do you agree with Jev?" votes (website/src/jev/agree.mjs), counted anonymously:
POST /jev/votes { a, b, pick }: two song ids as shown, and the
one the listener picked as more art
GET /jev/votes/summary { votes: [{ a, b, pick, n }] }: a count per
(a, b, pick), for nix run .#agreement
A vote is kept only as a count against its (a, b, pick) in the site's D1 database (votes-store.ts): no time, no address, no visitor. Songs must be the deploy's own (the site's /jev/songs.json), so the counts hold nothing a caller made up; the pick must be one of the two. Agreement with the art critic is not worked out here: it is always against the critic's current scores, which live in the repo (tools/agreement).
The site's song ids, built by website/src/pages/jev/songs.json.js.
The ids as songs.json writes them: "<theme>/<song>" folder names.
24const SONG_ID = /^[a-z0-9-]{1,64}\/[a-z0-9-]{1,64}$/;
Per visitor, set in wrangler.json. A vote is a click after reading two titles, and "another pair" is a click more, so a listener votes a few times a minute at most; the summary is read by a tool, once a run. Apart from JEV_LIMIT, so voting never spends a song's Jev calls.
30export const VOTE_LIMIT = config.ratelimits.find((r) => r.name === 'VOTE_LIMIT')!.simple;
32export type Vote = { a: string; b: string; pick: string }; 33export type Count = Vote & { n: number }; 34export interface VoteStore { 35 add(vote: Vote): Promise<void>; 36 counts(): Promise<Count[]>; 37} 38 39const isObject = (x: unknown): x is Record<string, unknown> => typeof x === 'object' && x !== null && !Array.isArray(x);
null when body is a vote on two known songs; otherwise why not.
42export function whyNotVote(body: unknown, known: ReadonlySet<string>): string | null { 43 if (!isObject(body)) return 'body must be a JSON object'; 44 const { a, b, pick, ...rest } = body; 45 if (Object.keys(rest).length) return `unexpected fields: ${Object.keys(rest).join(', ')}`; 46 if (typeof a !== 'string' || typeof b !== 'string' || typeof pick !== 'string') return 'a, b and pick are song ids'; 47 if (a === b) return 'a and b must be two songs'; 48 for (const id of [a, b]) if (!SONG_ID.test(id) || !known.has(id)) return `not a song on this site: ${id.slice(0, 64)}`; 49 if (pick !== a && pick !== b) return 'pick must be a or b'; 50 return null; 51}
The deploy's song ids: its own static file through the ASSETS binding in
production (and buck2 run //:preview), or the dev server's live one
(SONGS_URL, env.dev) in dev, where a new song is votable on reload.
Kept for a minute per isolate.
60export async function knownSongs(env: Env): Promise<ReadonlySet<string>> { 61 if (songs && Date.now() < songs.until) return songs.ids; 62 let res: Response; 63 if (env.SONGS_URL) res = await fetch(env.SONGS_URL); 64 else if (env.ASSETS) res = await env.ASSETS.fetch(new Request(new URL(SONGS_PATH, 'https://assets.invalid'))); 65 else throw new Error('no song list: neither SONGS_URL nor ASSETS is bound'); 66 if (!res.ok) throw new Error(`the song list answered ${res.status}`); 67 const { songs: ids } = (await res.json()) as { songs?: unknown }; 68 if (!Array.isArray(ids) || !ids.every((id) => typeof id === 'string')) throw new Error('the song list is malformed'); 69 songs = { ids: new Set(ids), until: Date.now() + SONGS_TTL_MS }; 70 return songs.ids; 71}
For tests: forget the cached list.
74export const forgetSongs = () => void (songs = null);
76const answer = (status: number, text: string, headers: Record<string, string> = {}) => 77 new Response(text, { status, headers: { 'Cache-Control': 'no-store', ...headers } }); 78 79export async function votes(request: Request, env: Env, store: VoteStore): Promise<Response> { 80 const { pathname } = new URL(request.url); 81 const summary = pathname === SUMMARY_PATH; 82 const method = summary ? 'GET' : 'POST'; 83 if (request.method !== method) return answer(405, `${method} only`, { Allow: method }); 84 85 const visitor = request.headers.get('CF-Connecting-IP') ?? 'local'; 86 const { success } = await env.VOTE_LIMIT.limit({ key: visitor }); 87 if (!success) return answer(429, 'too many votes; try again in a minute', { 'Retry-After': String(VOTE_LIMIT.period) }); 88 89 if (summary) { 90 return Response.json({ votes: await store.counts() }, { headers: { 'Cache-Control': 'no-store' } }); 91 } 92 93 const raw = await request.arrayBuffer(); 94 if (raw.byteLength > MAX_BYTES) return answer(413, 'vote too large'); 95 let body: unknown; 96 try { 97 body = JSON.parse(new TextDecoder().decode(raw)); 98 } catch { 99 return answer(400, 'body must be JSON'); 100 } 101 const cast = await castVote(env, store, body); 102 if (!cast.ok) return answer(cast.status, cast.why); 103 return new Response(null, { status: 204, headers: { 'Cache-Control': 'no-store' } }); 104}
One vote counted, once the caller has held it to VOTE_LIMIT: on the
deploy's own songs, the pick one of the two. The page's POST /jev/votes
(above, per address) and the MCPs' vote (shared-mcp.ts, per account)
both count through here, so both keep the same rules.
110export type Cast = { ok: true } | { ok: false; status: number; why: string }; 111export async function castVote(env: Env, store: VoteStore, body: unknown): Promise<Cast> { 112 let known: ReadonlySet<string>; 113 try { 114 known = await knownSongs(env); 115 } catch (e) { 116 console.error({ event: 'jev.votes', error: (e as Error).message }); 117 return { ok: false, status: 503, why: 'votes are not being counted right now' }; 118 } 119 const why = whyNotVote(body, known); 120 if (why) return { ok: false, status: 400, why }; 121 const { a, b, pick } = body as Vote; 122 await store.add({ a, b, pick }); 123 return { ok: true }; 124}