Opening Frasier on a Fairphone took ~5 s to render the episode list although every episode was cached. Three causes: - resolve_series_view waited for Next Up and resume before returning the episodes, and Next Up was server-first. The episode list now returns as soon as the episodes are in (with_hints); hints that have answered are used, late ones dropped, and the picker falls back to local watch state. Next Up is cache-first like every other query. - The page loaded itself six times per open: onMount plus a mount-time $effect, the reachability effect's first run posing as a reconnect, and a double mount. All triggers now share one coalesced load per item (createCoalescedLoader); refresh triggers get one re-run after it. - The root layout rendered the route in two branches that each rendered children; the page store deciding between them updates a flush late, so navigating Search -> library page mounted the page twice. One element now renders the route and only its classes change. On the device: one load per open, seasons from cache in 14 ms, episodes and the Resume button up in under a second (was ~5 s).
56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
/**
|
|
* Load one keyed thing at a time, however many triggers ask for it.
|
|
*
|
|
* Calls for the key already loading share that load instead of starting their
|
|
* own. A caller that knows the data changed (`fresh` — after "mark watched",
|
|
* on reconnect, when a filter flips) must not be answered by a load that may
|
|
* predate the change, so it gets exactly one re-run once the current load
|
|
* ends, however many such callers there were.
|
|
*
|
|
* Exists because the series page loaded itself six times on every open —
|
|
* `onMount`, a mount-time `$effect`, the reachability effect's first run and
|
|
* navigation updates each started a full load — putting about six times a
|
|
* dozen requests in flight at once.
|
|
*
|
|
* TRACES: UR-062 | DR-295
|
|
*/
|
|
export interface CoalescedLoader {
|
|
/** Load `key`. `fresh`: the caller knows the data changed. */
|
|
load(key: string, options?: { fresh?: boolean }): Promise<void>;
|
|
}
|
|
|
|
interface InFlight {
|
|
key: string;
|
|
/** Settles when this load and any re-run it owes have finished. */
|
|
done: Promise<void>;
|
|
rerun: boolean;
|
|
}
|
|
|
|
export function createCoalescedLoader(run: (key: string) => Promise<void>): CoalescedLoader {
|
|
let inFlight: InFlight | null = null;
|
|
|
|
return {
|
|
load(key, options = {}) {
|
|
if (inFlight && inFlight.key === key) {
|
|
if (options.fresh) inFlight.rerun = true;
|
|
return inFlight.done;
|
|
}
|
|
|
|
const entry: InFlight = { key, rerun: false, done: Promise.resolve() };
|
|
entry.done = (async () => {
|
|
try {
|
|
await run(key);
|
|
while (entry.rerun) {
|
|
entry.rerun = false;
|
|
await run(key);
|
|
}
|
|
} finally {
|
|
if (inFlight === entry) inFlight = null;
|
|
}
|
|
})();
|
|
inFlight = entry;
|
|
return entry.done;
|
|
},
|
|
};
|
|
}
|