jevstrudel: evaluations run one at a time and the newest wins
(repl.mjs, serialized); a stop or newer start during beforeStart
cancels a start (cyclist.mjs). Before, a visitor's first click released
the welcome song's evaluation beside the song they clicked, and whichever
set its pattern last played (2026-09-27).
11const deferred = () => { 12 let resolve; 13 const promise = new Promise((r) => (resolve = r)); 14 return { promise, resolve }; 15}; 16 17const settle = () => new Promise((r) => setTimeout(r, 0)); 18 19const quietRepl = (options) => 20 repl({ getTime: () => 0, setInterval: () => 0, clearInterval: () => {}, ...options }); 21 22describe('repl.evaluate', () => { 23 it('runs a newer evaluation only after the one before it settles, and skips the superseded one', async () => { 24 globalThis.__first = pure('first'); 25 globalThis.__second = pure('second'); 26 const gate = deferred(); 27 const calls = []; 28 let evals = 0; 29 const r = quietRepl({ 30 beforeEval: ({ code }) => { 31 calls.push(`before ${code}`); 32 // the first evaluation waits, as the welcome song waits for a click 33 return evals++ === 0 ? gate.promise : undefined; 34 }, 35 afterEval: ({ code }) => calls.push(`after ${code}`), 36 }); 37 const first = r.evaluate('globalThis.__first', false); 38 await settle(); 39 const second = r.evaluate('globalThis.__second', false); 40 await settle(); 41 // the second has not begun while the first waits 42 expect(calls).toEqual(['before globalThis.__first']); 43 gate.resolve(); 44 await Promise.all([first, second]); 45 expect(calls).toEqual(['before globalThis.__first', 'before globalThis.__second', 'after globalThis.__second']); 46 expect(r.scheduler.pattern.firstCycleValues).toEqual(['second']); 47 }); 48 49 it('skips an evaluation queued behind another when a newer one arrives', async () => { 50 globalThis.__a = pure('a'); 51 globalThis.__b = pure('b'); 52 globalThis.__c = pure('c'); 53 const gate = deferred(); 54 const before = []; 55 const r = quietRepl({ 56 beforeEval: ({ code }) => { 57 before.push(code); 58 return before.length === 1 ? gate.promise : undefined; 59 }, 60 }); 61 const runs = [r.evaluate('globalThis.__a', false)]; 62 await settle(); 63 runs.push(r.evaluate('globalThis.__b', false), r.evaluate('globalThis.__c', false)); 64 gate.resolve(); 65 await Promise.all(runs); 66 // b never began: c superseded it while a was still settling (and a, 67 // superseded in its beforeEval, set no pattern) 68 expect(before).toEqual(['globalThis.__a', 'globalThis.__c']); 69 expect(r.scheduler.pattern.firstCycleValues).toEqual(['c']); 70 }); 71}); 72 73describe('Cyclist.start', () => { 74 const cyclist = (beforeStart) => { 75 const c = new Cyclist({ getTime: () => 0, setInterval: () => 0, clearInterval: () => {}, beforeStart }); 76 c.pattern = pure(1); 77 c.clock.start = vi.fn(); 78 return c; 79 }; 80 81 it('does not start once stopped while beforeStart was awaited', async () => { 82 const gate = deferred(); 83 const c = cyclist(() => gate.promise); 84 const starting = c.start(); 85 c.stop(); 86 gate.resolve(); 87 await starting; 88 expect(c.started).toBe(false); 89 expect(c.clock.start).not.toHaveBeenCalled(); 90 }); 91 92 it('starts once, for the newest start', async () => { 93 const gates = [deferred(), deferred()]; 94 let n = 0; 95 const c = cyclist(() => gates[n++].promise); 96 const a = c.start(); 97 const b = c.start(); 98 gates[1].resolve(); 99 await b; 100 gates[0].resolve(); 101 await a; 102 expect(c.started).toBe(true); 103 expect(c.clock.start).toHaveBeenCalledTimes(1); 104 }); 105});