commit 2efb49c6bd4d8acc72684cd9e25f9c44ad86bbd5
parent 556dd6e71302e64ed7fda62410df421fc7ee8f10
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 16:40:01 -0400
common: a search restored from localStorage waits for the visitor
The session restored on load (query, filters, channel selection, profile)
is committed as before, so the profile, share-link and divergence logic see
it unchanged, but a restored non-empty query is HELD: the pipeline does not
run until the visitor presses Search / Enter or loads a profile. The results
area shows the browse listing under the restored filters and the bar shows
its existing "Press Enter or click Search to apply" line. A query on the URL
(qt=, legacy q=) is a shared link and still runs.
export/e2e/restore-no-refire.spec.ts proves no transcript shard is fetched
on load and that submitting fetches one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 150 insertions(+), 5 deletions(-)
diff --git a/common/components/SearchSessionContext.tsx b/common/components/SearchSessionContext.tsx
@@ -393,7 +393,15 @@ function useSearchSessionState() {
() => canonicalHash(committedRoot),
[committedRoot],
);
- const queryDirty = draftHash !== committedHash;
+ // A query restored from localStorage is committed (so the profile, share
+ // link and divergence logic all see the session exactly as it was left) but
+ // HELD: the pipeline does not run it until the visitor acts — Search / Enter,
+ // or a profile load. A restored query re-ran on every visit, refetching
+ // transcript shards (up to 8 MB each) on a cold device for a search nobody
+ // asked for this time. A query that arrives on the URL (`qt=`, legacy `q=`)
+ // is never held: a shared link is the visitor asking.
+ const [runHeld, setRunHeld] = useState(false);
+ const queryDirty = runHeld || draftHash !== committedHash;
const [draftExcludedChannels, setDraftExcludedChannels] = useState<
Set<string>
@@ -789,9 +797,11 @@ function useSearchSessionState() {
[committedTags, postScopeSlugs],
);
+ // False while a restored query is held (see `runHeld`): the results area
+ // shows the browse listing under the restored filters, and nothing fetches.
const hasActiveQuery = useMemo(
- () => isNodeActive(committedRoot),
- [committedRoot],
+ () => !runHeld && isNodeActive(committedRoot),
+ [committedRoot, runHeld],
);
// ─── Run the query tree ────────────────────────────────────────────────────
@@ -835,6 +845,7 @@ function useSearchSessionState() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
committedHash,
+ hasActiveQuery,
transcripts,
filterKey,
needsChatManifests,
@@ -1201,6 +1212,8 @@ function useSearchSessionState() {
}
let resolvedRoot = rootFromUrl;
+ // True only when the query came from the stored snapshot, never the URL.
+ let holdRestored = false;
if (!initialExcluded) {
// Fall back to stored profile / working snapshot.
const snapshot =
@@ -1223,13 +1236,15 @@ function useSearchSessionState() {
initialTags ??= new Set(snapshot?.tg ?? []);
if (!resolvedRoot && snapshot?.query) {
resolvedRoot = parseRoot(snapshot.query);
+ holdRestored = resolvedRoot != null && isNodeActive(resolvedRoot);
}
}
if (resolvedRoot) {
setDraftRoot(resolvedRoot);
setCommittedRoot(resolvedRoot);
- setSearchExecuted(true);
+ if (holdRestored) setRunHeld(true);
+ else setSearchExecuted(true);
}
setDraftExcludedChannels(initialExcluded);
@@ -1259,7 +1274,9 @@ function useSearchSessionState() {
if (csRaw) {
setChartShape(parseChartShape(csRaw));
} else if (wantsChart) {
- const active = resolvedRoot ? isNodeActive(resolvedRoot) : false;
+ // A held query has no results to chart until it runs.
+ const active =
+ resolvedRoot && !holdRestored ? isNodeActive(resolvedRoot) : false;
setChartShape(defaultChartShape(active ? "search" : "metadata"));
}
if (wantsChart) setView("chart");
@@ -1342,6 +1359,7 @@ function useSearchSessionState() {
const commitSearch = () => {
setSearchExecuted(true);
+ setRunHeld(false);
setCommittedRoot(draftRoot);
const nextSnapshot = buildDraftSnapshot();
const current = loadStoredState() ?? emptyStoredState();
@@ -1403,6 +1421,8 @@ function useSearchSessionState() {
const applySnapshot = useCallback(
(snapshot: FilterSnapshot | null) => {
+ // Loading a profile commits it, which runs it — as it always has.
+ setRunHeld(false);
applyDraftSnapshot(snapshot);
const excluded = snapshotToExcluded(
snapshot,
diff --git a/export/e2e/restore-no-refire.spec.ts b/export/e2e/restore-no-refire.spec.ts
@@ -0,0 +1,125 @@
+import { expect, test, type Page } from "@playwright/test";
+import { installRoutes } from "./helpers";
+
+// Release 8 slice E. The search session restored from localStorage loads the
+// query and the filters but does NOT run the search on first load: a restored
+// query used to re-run the pipeline on every visit, refetching transcript
+// shards (up to 8 MB each on a cold device) for a search the visitor had not
+// asked for this time. The visitor runs it — Search / Enter. A query on the
+// URL (`qt=`) is a shared link, i.e. the visitor asking, and still runs.
+
+const STORAGE_KEY = "ytdlp-tb:export-filters";
+const SHARD = /\/transcripts\/[^/]+\/page-\d+\.json$/;
+
+function countShards(page: Page): { n: number } {
+ const seen = { n: 0 };
+ page.on("request", (req) => {
+ if (SHARD.test(new URL(req.url()).pathname)) seen.n += 1;
+ });
+ return seen;
+}
+
+const leafInput = (page: Page) =>
+ page.locator('input[data-testid^="leaf-query-"]').first();
+
+// A query that hits every fixture transcript (each has an "alpha line" cue),
+// in the app's own serialization (`stringifyRoot`: the qt= JSON).
+const ALPHA_TREE = { k: "g", o: "AND", c: [{ k: "l", q: "alpha", s: "transcripts" }] };
+
+// Pre-seed the session a previous visit would have left: the query plus a
+// filter (Livestreams off). Written via page.evaluate on a throwaway first load
+// — not addInitScript, which would re-fire on every navigation. Nothing has
+// searched in this browser context, so the IndexedDB layer cache is empty and a
+// search that runs MUST fetch shards.
+async function seedStoredSearch(page: Page) {
+ await page.goto("/");
+ await page.evaluate(
+ ({ key, value }) => window.localStorage.setItem(key, value),
+ {
+ key: STORAGE_KEY,
+ value: JSON.stringify({
+ v: 1,
+ working: {
+ channels: { included: [], excluded: [] },
+ nol: true,
+ query: JSON.stringify(ALPHA_TREE),
+ },
+ profiles: {},
+ activeProfileName: null,
+ }),
+ },
+ );
+}
+
+test.describe("restored search waits for the visitor", () => {
+ test.beforeEach(async ({ page }) => {
+ await installRoutes(page);
+ });
+
+ test("a restored query loads into the form, fetches no shard, and runs on submit", async ({
+ page,
+ }) => {
+ const shards = countShards(page);
+ await seedStoredSearch(page);
+
+ // A fresh visit with no query on the URL: the session comes from storage.
+ shards.n = 0;
+ await page.goto("/");
+ // The settings load: the query is in the input, the filter is restored…
+ await expect(leafInput(page)).toHaveValue("alpha");
+ await expect(
+ page.getByRole("checkbox", { name: "Livestreams" }),
+ ).not.toBeChecked();
+ // …and the bar says what the visitor has to do, as for any unapplied edit.
+ await expect(page.getByTestId("search-submit")).toHaveAttribute(
+ "data-dirty",
+ "true",
+ );
+ await expect(
+ page.getByText("Press Enter or click Search to apply"),
+ ).toBeVisible();
+ // The results area is the browse listing under the restored filters.
+ await expect(page.getByTestId("results-summary")).toHaveText(
+ "All videos (3)",
+ );
+ await expect(page.getByTestId("browse-hint")).toBeVisible();
+ // Give a would-be pipeline every chance to start.
+ await page.waitForTimeout(1_500);
+ expect(shards.n).toBe(0);
+
+ // The visitor runs it.
+ const fetched = page.waitForRequest(
+ (req) => SHARD.test(new URL(req.url()).pathname),
+ { timeout: 10_000 },
+ );
+ await leafInput(page).press("Enter");
+ await fetched;
+ await expect(page.getByTestId("results-summary")).toContainText(
+ "Matching videos",
+ );
+ await expect(page.getByTestId("search-submit")).toHaveAttribute(
+ "data-dirty",
+ "false",
+ );
+ });
+
+ test("a query on the URL (a shared link) still runs on load", async ({
+ page,
+ }) => {
+ const shards = countShards(page);
+ // A stored session is present too: the URL query is not held, it runs.
+ await seedStoredSearch(page);
+ const link = `/?qt=${encodeURIComponent(JSON.stringify(ALPHA_TREE))}`;
+
+ shards.n = 0;
+ await page.goto(link);
+ await expect(page.getByTestId("results-summary")).toContainText(
+ "Matching videos",
+ );
+ await expect.poll(() => shards.n).toBeGreaterThan(0);
+ await expect(page.getByTestId("search-submit")).toHaveAttribute(
+ "data-dirty",
+ "false",
+ );
+ });
+});