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,57 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { api, type TakeoutStatus } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
// A Takeout import, watched to its end: its status is asked again until no run is queued or going,
|
||||
// then the lists it changed are read afresh. It lives outside the section on purpose: the user may
|
||||
// leave Settings while the import runs, and the feed they go to must not keep showing as new what
|
||||
// the import has just marked watched.
|
||||
|
||||
const POLL_MS = 1500;
|
||||
|
||||
export const importBusy = (s: TakeoutStatus | undefined): boolean =>
|
||||
!!s && (s.queued || s.run?.status === "running");
|
||||
|
||||
let watching = false;
|
||||
|
||||
const pause = () => new Promise<void>((done) => setTimeout(done, POLL_MS));
|
||||
|
||||
async function read(qc: QueryClient): Promise<TakeoutStatus | undefined> {
|
||||
try {
|
||||
return await qc.fetchQuery({
|
||||
queryKey: qk.takeout(),
|
||||
queryFn: api.takeoutStatus,
|
||||
staleTime: 0,
|
||||
});
|
||||
} catch {
|
||||
// A read that failed is the section's to say (its query holds the error); the watch asks again.
|
||||
return qc.getQueryData<TakeoutStatus>(qk.takeout());
|
||||
}
|
||||
}
|
||||
|
||||
/** Mark stale everything an import may have changed: the feed and its counts, Watch later, and the
|
||||
* playlists that show its size. */
|
||||
export function invalidateImported(qc: QueryClient): void {
|
||||
for (const queryKey of [
|
||||
qk.feed(),
|
||||
qk.feedSummary(),
|
||||
qk.feedYears(),
|
||||
qk.playlist(),
|
||||
qk.playlists(),
|
||||
])
|
||||
void qc.invalidateQueries({ queryKey });
|
||||
}
|
||||
|
||||
/** Watch the user's import until it is done (once: a second call while it watches is that watch),
|
||||
* then mark stale what it changed. */
|
||||
export async function watchTakeout(qc: QueryClient): Promise<void> {
|
||||
if (watching) return;
|
||||
watching = true;
|
||||
let status = qc.getQueryData<TakeoutStatus>(qk.takeout());
|
||||
while (importBusy(status)) {
|
||||
await pause();
|
||||
status = await read(qc);
|
||||
}
|
||||
watching = false;
|
||||
invalidateImported(qc);
|
||||
}
|
||||
Reference in New Issue
Block a user