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.
194 lines
11 KiB
JavaScript
194 lines
11 KiB
JavaScript
// Round thirty-two: the phone's own weight. Thirty rounds of onMount had
|
|
// never been read end to end, and read end to end it was a queue — two
|
|
// dozen commands with a critical path ten deep, because Tauri's `listen`
|
|
// is an invoke like any other: five independent SQLite reads one behind
|
|
// the other in loadLibrary, eight listeners one behind the other in
|
|
// bindSlsk, two more each in watchLibrary and the player's bindEvents.
|
|
// Nothing here depended on anything before it. They go out in one round
|
|
// now — the same commands, ten of which used to leave in the shell's own
|
|
// first turn where twenty-three do, and the critical path drops to three.
|
|
// Nothing else changes: the round is meant to be invisible except that the
|
|
// app starts sooner.
|
|
//
|
|
// So that it stays that way, the round also adds the project's first boot
|
|
// instrument — one counter every invoke and every listen passes, sealed at
|
|
// the first frame the shell paints, and a budget in the mock walk held
|
|
// against it, whose sharpest clause (bootSync) has no clock in it at all.
|
|
// perf.mjs measures frames; nothing measured the launch.
|
|
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);
|
|
|
|
/* Several of the replacements below re-emit their own anchors — the census
|
|
lines in api.js and events.js, the Promise.all bodies — so `once` cannot
|
|
see a second run coming and would throw only halfway through, leaving a
|
|
half-patched tree. Refuse one here, before anything is written.
|
|
src/lib/boot.js is created by this round and by nothing else. */
|
|
if (fs.existsSync(root + 'src/lib/boot.js')) {
|
|
throw new Error('round 32 has already run against ' + root + ' — src/lib/boot.js exists; reset the tree rather than re-running');
|
|
}
|
|
|
|
// ── the census itself ─────────────────────────────────────────────────
|
|
fs.writeFileSync(root + 'src/lib/boot.js', ins('ins-r32-boot.txt') + NL);
|
|
|
|
// ── one counter on the way past the bridge, both doors ────────────────
|
|
load('src/lib/api.js');
|
|
rep(`import { mockInvoke } from "./mockLibrary.js";`,
|
|
`import { mockInvoke } from "./mockLibrary.js";
|
|
import { countBoot } from "./boot.js";`);
|
|
rep(`const invoke = (cmd, args) => mockInvoke(cmd, args) ?? tauriInvoke(cmd, args);`,
|
|
`/* the boot census (src/lib/boot.js) counts what the app asks for before its
|
|
first frame and stops there, so this is one comparison for the rest of the
|
|
run. Inert on the desktop: nothing seals it there and nothing reads it. */
|
|
const invoke = (cmd, args) => { countBoot(cmd); return mockInvoke(cmd, args) ?? tauriInvoke(cmd, args); };`);
|
|
save();
|
|
|
|
load('src/lib/events.js');
|
|
rep(`import { mockListen } from "./mockLibrary.js";`,
|
|
`import { mockListen } from "./mockLibrary.js";
|
|
import { countBoot } from "./boot.js";`);
|
|
rep(`export const listen = (event, cb) => mockListen(event, cb) ?? tauriListen(event, cb);`,
|
|
`/* a listen is an invoke too — Tauri registers the subscription over the
|
|
same bridge — so the boot census counts it as one */
|
|
export const listen = (event, cb) => { countBoot("listen " + event); return mockListen(event, cb) ?? tauriListen(event, cb); };`);
|
|
save();
|
|
|
|
// ── loadLibrary: five reads in one round ──────────────────────────────
|
|
load('src/mobile/lib.svelte.js');
|
|
rep(` try {
|
|
const st = await api.appStatus();
|
|
lib.version = st?.version ?? "";
|
|
lib.username = st?.connected_as ?? null;
|
|
} catch (e) { lib.error = String(e); }
|
|
try { lib.albums = await api.libraryAlbums(); } catch (e) { lib.error = String(e); }
|
|
await refreshRecents();
|
|
try { lib.follows = (await api.followedArtists()) || []; } catch { lib.follows = []; }
|
|
try { lib.playlists = (await api.listPlaylists()) || []; } catch { lib.playlists = []; }
|
|
lib.loaded = true;
|
|
void refreshStats();`, ins('ins-r32-loadlibrary.txt'));
|
|
// ── watchLibrary: the two library subscriptions in one ────────────────
|
|
rep(` await listen("soulseek:library-changed", () => {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(async () => {
|
|
try { lib.albums = await api.libraryAlbums(); } catch { /* keep */ }
|
|
await refreshRecents();
|
|
void refreshStats();
|
|
}, 600);
|
|
});
|
|
/* a playlist mutation (the desktop's commands emit it) re-lists them */
|
|
await listen("soulseek:playlists-changed", () => { void refreshPlaylists(); });`,
|
|
ins('ins-r32-watchlibrary.txt'));
|
|
save();
|
|
|
|
// ── bindSlsk: eight listeners in one round ────────────────────────────
|
|
// Every handler body is left exactly as it was; only the shape around
|
|
// them changes, and the eight `await`s come off the call sites (an
|
|
// `await` on the new `on` would put the queue straight back).
|
|
load('src/mobile/slsk.svelte.js');
|
|
rep(` const subs = [];
|
|
const on = async (name, fn) => { try { subs.push(await listen(name, (ev) => fn(ev.payload))); } catch { /* not a phone */ } };
|
|
await on("soulseek:state", (ev) => {`, ins('ins-r32-slsk-on.txt'));
|
|
for (const ev of ['room-list', 'room-joined', 'room-left', 'room-user', 'room-message', 'private-message', 'user-state']) {
|
|
rep(` await on("soulseek:` + ev + `",`, ` on("soulseek:` + ev + `",`);
|
|
}
|
|
rep(` return subs;`,
|
|
` /* the one ordering that matters: connectStored() runs off this promise,
|
|
so a fast connected state must not land with nobody listening */
|
|
await Promise.all(pending);
|
|
return subs;`);
|
|
save();
|
|
|
|
// ── the player's two, likewise ────────────────────────────────────────
|
|
load('src/mobile/player.svelte.js');
|
|
rep(` try {
|
|
await listen("soulseek:stream", (ev) => {
|
|
const p = ev.payload || {};
|
|
if (!current()?.t?.stream) return;
|
|
player.stream = { received: p.received || 0, total: p.total || 0, done: !!p.done, failed: p.failed || null };
|
|
if (p.failed) setErr("The stream from " + current().t.stream.username + " failed: " + p.failed);
|
|
});
|
|
} catch { /* not a phone */ }
|
|
return await listen("soulseek:player", (ev) => {`,
|
|
ins('ins-r32-player-stream.txt') + NL + ` const transportSub = listen("soulseek:player", (ev) => {`);
|
|
rep(` case "error":
|
|
player.error = p.message || "playback error";
|
|
player.status = "idle";
|
|
break;
|
|
}
|
|
});
|
|
}`,
|
|
` case "error":
|
|
player.error = p.message || "playback error";
|
|
player.status = "idle";
|
|
break;
|
|
}
|
|
` + ins('ins-r32-player-tail.txt'));
|
|
save();
|
|
|
|
// ── the shell: the marks, the seal, and the handle ────────────────────
|
|
load('src/mobile/Mobile.svelte');
|
|
rep(` import * as sources from "../lib/sources.js";`,
|
|
` import * as sources from "../lib/sources.js";
|
|
import { boot, sealBoot } from "../lib/boot.js";`);
|
|
rep(` onMount(() => {
|
|
loadPrefs();
|
|
bootNav();`, ins('ins-r32-mobile-onmount.txt'));
|
|
rep(` /* the probe handle — the desktop has __soulstream for the same reason:
|
|
tests/mobile/smoke.mjs drives the real app through it */
|
|
window.__ss = { nav, top, go, push, back, openSheet, closeSheet, openHalf, closeHalf, player, lib, art, catalog, slsk,`,
|
|
ins('ins-r32-mobile-seal.txt') + NL + ` /* the probe handle — the desktop has __soulstream for the same reason:
|
|
tests/mobile/smoke.mjs drives the real app through it */
|
|
window.__ss = { nav, top, go, push, back, openSheet, closeSheet, openHalf, closeHalf, player, lib, art, catalog, slsk, boot,`);
|
|
rep(` onMount(() => {`, ins('ins-r32-mobile-effect.txt') + NL + NL + ` onMount(() => {`);
|
|
save();
|
|
|
|
// ── the one dead import of the ten whose reason is the bundle. Rollup
|
|
// drops an unreferenced NAMED import by itself, so the other nine are
|
|
// hygiene rather than weight and go out with round thirty-three's sweep,
|
|
// which is the round for code nothing references. A NAMESPACE import is
|
|
// the one shape tree-shaking cannot see through, and this one is dead.
|
|
// It frees nothing by itself — twenty-nine other files in the phone's
|
|
// graph hold api.js the same way — but it is the only one of the ten
|
|
// that belongs to a round about weight, so it goes here.
|
|
load('src/mobile/Sheet.svelte');
|
|
rep(` import * as api from "../lib/api.js";` + NL, ``);
|
|
save();
|
|
|
|
// ── the walk: the census, and a budget on both legs ───────────────────
|
|
load('tests/mobile/smoke.mjs');
|
|
rep(` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),`,
|
|
` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),
|
|
` + ins('ins-r32-probe.txt'));
|
|
// Home is the first named step and waits 1600 ms, so the census has been
|
|
// sealed for well over a second by the time this probe lands.
|
|
rep(`)?$/.test(i.navTabs) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`,
|
|
`)?$/.test(i.navTabs) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState
|
|
` + ins('ins-r32-mock-expect.txt'));
|
|
rep(`const PHONE_EXPECT = {`, `const PHONE_EXPECT = {` + NL + ins('ins-r32-phone-expect.txt'));
|
|
save();
|
|
|
|
// ── the doc ───────────────────────────────────────────────────────────
|
|
load('ANDROID.md');
|
|
rep(`The audio side had its own lag:`, ins('ins-r32-doc.txt') + NL + NL + `The audio side had its own lag:`);
|
|
rep(`asserts what each step should show, screenshots every step into \`tests/mobile/out/\``,
|
|
`asserts what each step should show — the Home
|
|
step holds the launch to a budget as well, reading off \`__ss.boot\` how many
|
|
commands the app issued before its first frame, how many of those left in the
|
|
shell's own turn, how long it took, and requiring round 12's art prewarm to
|
|
still be absent from them — screenshots every step into \`tests/mobile/out/\``);
|
|
save();
|
|
|
|
console.log('round 32 wired');
|