Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
141 lines
8.2 KiB
JavaScript
141 lines
8.2 KiB
JavaScript
// Round forty-one: the long session. Every walk this project has ever run is
|
|
// minutes long; a phone runs the app for hours, and nothing in the port had
|
|
// been looked at for a leak. Driving a real build of this tree for eleven
|
|
// laps says the listeners and the intervals are dead flat — the naive
|
|
// add-minus-remove counter climbs twelve a lap and every one of them is a
|
|
// `.screen` element {#key nav.seq} threw away — and that exactly two things
|
|
// grow without a limit: the thumbnail map, which kept every cover it had
|
|
// ever fetched for the WebView's lifetime, and the playlist cover table,
|
|
// which strands one data URL per edit under a key nothing can read again.
|
|
// Both are bounded here at the desktop's own numbers, with two extra rules
|
|
// the desktop does not need because the phone's templates index the map
|
|
// instead of asking through a getter. Then the instrument: artTally() on the
|
|
// handle, seventeen probe fields, and seven mock steps that install a live
|
|
// listener/timer registry in the page, drive two warm laps, a baseline and
|
|
// nine more, assert the census did not grow, and take the registry back out
|
|
// again. No new UI, no CSS, no Rust, no Kotlin.
|
|
const fs = require('fs');
|
|
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
|
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
|
|
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
|
|
let p, s;
|
|
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
|
const save = () => fs.writeFileSync(root + p, s);
|
|
const once = (a) => {
|
|
const i = s.indexOf(a);
|
|
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
|
|
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
|
|
return i;
|
|
};
|
|
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
|
|
const NL = String.fromCharCode(10);
|
|
|
|
/* The tally is APPENDED to art.svelte.js with no anchor of its own, and
|
|
several inserts below re-emit their own anchor, so a second run would
|
|
write twice where `once` cannot see it coming and would only throw
|
|
further down, leaving half a tree. Refuse one here, before a byte is
|
|
written: `artTally` appears nowhere in the repo today and is the first
|
|
thing this round writes. */
|
|
if (fs.readFileSync(root + 'src/mobile/art.svelte.js', 'utf8').includes('artTally')) {
|
|
throw new Error('round 41 is already applied in ' + root + ' — reset the tree rather than re-running');
|
|
}
|
|
/* PROBE is ONE template literal, so a raw backtick anywhere inside it — a
|
|
comment included — ends it, and what follows becomes a tagged template:
|
|
the file still parses, `node --check` says nothing at all, and smoke.mjs
|
|
throws the moment it is imported. `${` is the same trap the other way.
|
|
Round thirty-nine's check, kept. */
|
|
for (const bad of ['`', '${']) {
|
|
if (ins('ins-r41-probe.txt').includes(bad)) {
|
|
throw new Error('ins-r41-probe.txt carries a ' + bad + ' — the probe block lives inside a template literal');
|
|
}
|
|
}
|
|
|
|
// ── the thumbnail map: bounded where it grows, and countable ──────────
|
|
load('src/mobile/art.svelte.js');
|
|
rep(`const pending = new Map(); // path -> { promise, resolve }`,
|
|
ins('ins-r41-art-trim.txt') + NL + NL + `const pending = new Map(); // path -> { promise, resolve }`);
|
|
/* the one call site: the end of pump()'s own .then, AFTER the waiter has
|
|
been handed the url it asked for, and told which key just landed so that
|
|
the entry a consumer is about to read can never be the one thrown away */
|
|
rep(` art[p] = u;
|
|
const w = pending.get(p);
|
|
pending.delete(p);
|
|
if (w) w.resolve(u);`,
|
|
` art[p] = u;
|
|
const w = pending.get(p);
|
|
pending.delete(p);
|
|
if (w) w.resolve(u);
|
|
/* bounded here, where it grows, and nowhere else */
|
|
trimArt(p);`);
|
|
/* the accents: the same budget, in insertion order, with none of the
|
|
painting question — nothing renders a promise */
|
|
rep(` );
|
|
}
|
|
return accents.get(path);`,
|
|
` );
|
|
while (accents.size > ART_MAX) accents.delete(accents.keys().next().value);
|
|
}
|
|
return accents.get(path);`);
|
|
/* appended, so the guard above is what refuses a second run */
|
|
s = s.replace(/\s*$/, NL) + NL + ins('ins-r41-art-tally.txt') + NL;
|
|
save();
|
|
|
|
// ── the playlist covers a revision left behind ────────────────────────
|
|
load('src/mobile/playlists.svelte.js');
|
|
rep(` .then((u) => { pls.covers[k] = u || null; }, () => { pls.covers[k] = null; })`,
|
|
` .then((u) => { pls.covers[k] = u || null; }, () => { pls.covers[k] = null; })
|
|
/* the key is "id:revision" and an edit bumps the revision, so
|
|
every earlier revision of this playlist is unreachable by
|
|
construction — nothing looks a playlist's cover up by
|
|
anything but its current one. Dropped as the new one lands:
|
|
no cap, no policy, just the keys that are already dead. */
|
|
.then(() => { const mine = p.id + ":"; for (const old of Object.keys(pls.covers)) if (old !== k && old.startsWith(mine)) delete pls.covers[old]; })`);
|
|
save();
|
|
|
|
// ── the seam: one name on the handle, and nothing else ────────────────
|
|
load('src/mobile/Mobile.svelte');
|
|
rep(` import { art, wantArt, accentFor, applyAccent } from "./art.svelte.js";`,
|
|
` import { art, artTally, wantArt, accentFor, applyAccent } from "./art.svelte.js";`);
|
|
/* deliberately a THIRD fragment of this literal: round thirty-nine took the
|
|
head (`art, catalog, slsk, boot,`) and round thirty-four the tail
|
|
(`wifi: { downloadsAllowed, cellular }, next, prev, toggle`), so a sibling
|
|
round appending to the handle reaches for one of those, not for this */
|
|
rep(`xfn: xfernote, pls, notice, importer,`, `xfn: xfernote, pls, notice, importer, artTally,`);
|
|
save();
|
|
|
|
// ── the walk ──────────────────────────────────────────────────────────
|
|
load('tests/mobile/smoke.mjs');
|
|
/* the second-to-last line of the probe, NOT the `contactsLoaded:` line
|
|
(rounds thirty-one and thirty-four) nor the `lyrTools:` line (round
|
|
thirty-nine), and not the probe's last line either, so a sibling
|
|
appending at the end of the object does not land on top of this */
|
|
rep(` miniLeft: Math.round((box('.mini') || { left: -1 }).left),`,
|
|
` miniLeft: Math.round((box('.mini') || { left: -1 }).left),` + NL + ins('ins-r41-probe.txt'));
|
|
/* the walk's current last step, NOT `];\nconst MOCK_EXPECT = {`, which round
|
|
thirty-four used and a sibling is likely to reuse: anchoring on the step
|
|
means a round appending at the array end lands after my block rather than
|
|
inside it */
|
|
rep(` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "export-clear", wait: 900 },`,
|
|
` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "export-clear", wait: 900 },`
|
|
+ NL + ins('ins-r41-steps.txt'));
|
|
rep(` "export-clear": (i) => /^(Good |Still)/.test(i.title) && !i.toast && !i.notice && !i.half.includes("up"),`,
|
|
` "export-clear": (i) => /^(Good |Still)/.test(i.title) && !i.toast && !i.notice && !i.half.includes("up"),`
|
|
+ NL + ins('ins-r41-expect.txt'));
|
|
save();
|
|
|
|
// ── the doc ───────────────────────────────────────────────────────────
|
|
/* the last paragraph of ## Performance and the heading after it — away from
|
|
"Not done yet" and from the walk's own step-list sentence, both of which
|
|
rounds thirty-four and thirty-nine already rewrote and which siblings in
|
|
this wave are likely to reach for again */
|
|
load('ANDROID.md');
|
|
rep(`The audio side had its own lag: \`dumpsys media.audio_flinger\` showed the
|
|
track on the fast mixer with runs of ~55 ms underruns. The 8192-frame buffer
|
|
above moved it to the normal mixer; the phone walk samples the underrun
|
|
counters before and after and fails if they rose.
|
|
|
|
## Prerequisites (home machine, done 2026-09-12)`, ins('ins-r41-doc.txt'));
|
|
save();
|
|
|
|
console.log('round 41 wired');
|