fix(reader): the Takeout import holds up under abuse, races and a run it cannot see
- Uploads: one at a time per user and ten an hour, each refused only after its body arrived; the history read an entry at a time (32 MiB cap), a Watch later list capped at YouTube's 5000, a user's kept videos at 200k; any unreadable shape is a 422, not a 500. - Quota: Watch later lookups stop at the backfill reserve and at a per-user daily share; what is not asked about waits, known videos still go in, and the shorts job retries the rest. - Staging and settling share one per-user advisory lock (no deadlock); a run settles only the rows it read, so a row staged meanwhile is never called gone. - A failed run's notice names a translated failure, never the upstream's text. - The status says when an import is queued; the page watches it to its end even when left, then reads the feed, its counts and the playlists again. - Settings: one live region, the zone kept (unavailable) while a run goes, focus handed back, a missed drop kept from the browser, an oversize file refused before it is sent. - Scheduler and run lists show Takeout runs by the importing user.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A Takeout import is watched to its end whether or not its section is still on screen, and what it
|
||||
// changed is read afresh then: the feed and its counts, Watch later, and the playlists' sizes.
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { api, type TakeoutStatus } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { importBusy, watchTakeout } from "./takeoutWatch";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const st = (s: Partial<TakeoutStatus>): TakeoutStatus => ({
|
||||
queued: false,
|
||||
max_bytes: 1,
|
||||
run: null,
|
||||
...s,
|
||||
});
|
||||
const running = {
|
||||
status: "running",
|
||||
started_at: "S",
|
||||
finished_at: null,
|
||||
result: null,
|
||||
failure: null,
|
||||
};
|
||||
|
||||
describe("the Takeout import's watch", () => {
|
||||
it("a queued import is busy as much as a running one", () => {
|
||||
expect(importBusy(st({ queued: true }))).toBe(true);
|
||||
expect(importBusy(st({ run: { ...running, status: "running" } as TakeoutStatus["run"] }))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(importBusy(st({ run: { ...running, status: "ok" } as TakeoutStatus["run"] }))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(importBusy(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("asks until the import is done, then reads again everything it changed", async () => {
|
||||
vi.useFakeTimers();
|
||||
const qc = new QueryClient();
|
||||
qc.setQueryData(qk.takeout(), st({ queued: true }));
|
||||
const answers = [st({ run: running as TakeoutStatus["run"] }), st({})];
|
||||
const read = vi.spyOn(api, "takeoutStatus").mockImplementation(async () => answers.shift()!);
|
||||
const stale = vi.spyOn(qc, "invalidateQueries");
|
||||
|
||||
const done = watchTakeout(qc);
|
||||
void watchTakeout(qc); // a second call while it watches is the same watch
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
expect(read).toHaveBeenCalledTimes(1);
|
||||
expect(stale).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
await done;
|
||||
|
||||
expect(read).toHaveBeenCalledTimes(2);
|
||||
expect(stale.mock.calls.map(([f]) => f?.queryKey)).toEqual([
|
||||
qk.feed(),
|
||||
qk.feedSummary(),
|
||||
qk.feedYears(),
|
||||
qk.playlist(),
|
||||
qk.playlists(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user