diff --git a/modules/spl-meter/module.json b/modules/spl-meter/module.json index e469e98..520978a 100644 --- a/modules/spl-meter/module.json +++ b/modules/spl-meter/module.json @@ -1,7 +1,7 @@ { "id": "spl-meter", "name": "SPL Meter", - "version": "0.3.0", + "version": "0.3.1", "proddash": ">=1.5.0", "description": "Calibrated sound level, Leq and RTA from an audio input on this machine", "permissions": [ diff --git a/modules/spl-meter/server.js b/modules/spl-meter/server.js index d3609a4..98fb47c 100644 --- a/modules/spl-meter/server.js +++ b/modules/spl-meter/server.js @@ -132,6 +132,7 @@ module.exports = { moduleDir, log, onUpdate(update) { + if (!state) return; // a frame in flight when the module unmounted if (update.error) { if (update.error !== state.error) log(update.error); state.error = update.error; @@ -146,9 +147,10 @@ module.exports = { }, }); + const mineStreams = state.streams; state.pinger = setInterval(() => { - for (const res of state.streams) { - try { res.write(': ping\n\n'); } catch { state.streams.delete(res); } + for (const res of mineStreams) { + try { res.write(': ping\n\n'); } catch { mineStreams.delete(res); } } }, PING_MS); @@ -260,8 +262,14 @@ module.exports = { try { res.write(`event: state\ndata: ${JSON.stringify(snapshot())}\n\n`); } catch { return; } - state.streams.add(res); - req.on('close', () => state.streams.delete(res)); + // Hold this mount's set, not `state`. teardown() nulls `state` and + // then ends every stream, which fires these close handlers — a + // handler that reached through `state` threw on null and took the + // whole server down with it, on any re-init with a browser + // connected. Deleting from a set nobody reads any more is harmless. + const streams = state.streams; + streams.add(res); + req.on('close', () => streams.delete(res)); }, /** Device list for the admin page. Runs the capture tool's --list, @@ -311,6 +319,7 @@ module.exports = { const samples = []; const started = Date.now(); const tick = setInterval(() => { + if (!state) { clearInterval(tick); return; } // module unmounted mid-measure const ch = state.frame && state.frame.ch.find((c) => c.i === wanted); if (ch && Number.isFinite(ch.dbfs) && ch.dbfs > -200) samples.push(ch.dbfs); if (Date.now() - started < MEASURE_MS) return; @@ -343,6 +352,7 @@ module.exports = { /* ── helpers ────────────────────────────────────────────────────────── */ function snapshot() { + if (!state) return { error: 'not measuring', calibrated: false, channels: [], frame: null }; return { error: state.error, calibrated: state.calibrated, @@ -356,6 +366,7 @@ function snapshot() { } function broadcast() { + if (!state) return; // a listener callback landing after teardown const payload = snapshot(); const line = JSON.stringify(payload); if (line === state.lastSent) return; // nothing moved since last frame