Sync says what it is doing, file by file
The counters said how MUCH and never which file, so a run that was working normally and one that had wedged produced the same screen: `done` stops moving either way. A photograph is around thirty encodes and a video is three plus a poster ladder, and none of that was visible from outside. So the run keeps a transcript and the button opens it. Every rung reports itself by CONSTRUCTION. Ten `variants.push` call sites became one `add`, which is the point — a rung added later is described without anybody remembering to describe it. The slow stages that produce nothing until they finish say so BEFORE they start instead: the HEIC decode, each video rung, the poster frame. Those are the pauses that read as a hang. `?since=<n>` is what keeps it cheap. The transcript is thousands of lines and polls once a second, so sending all of it every tick would be the same megabyte over and over; the client keeps what it has and asks for the rest. No `since` means everything, which is what a modal opened mid-run wants. The transcript lives in the shared slot, not the modal, so closing it does not throw away the run — somebody who looks, closes, and looks again is watching one run rather than two. `startedAt` is what identifies a run, because `n` restarts at 1 and appending across a boundary would interleave two. Polling, not a socket. Each line transfers once and the events are seconds apart, so a second transport to open, authenticate and reconnect would buy nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+59
-3
@@ -31,6 +31,17 @@ import { readableError } from './errors';
|
||||
|
||||
export type SyncError = { file: string; message: string };
|
||||
|
||||
/** One line of the run's transcript. Mirrors LogLine in api/sync.ts. */
|
||||
export type LogLine = {
|
||||
/** Monotonic within a run. What `?since=` is asked with, so each line transfers once. */
|
||||
n: number;
|
||||
at: string;
|
||||
level: 'info' | 'step' | 'done' | 'warn' | 'error';
|
||||
/** The file this is about, or blank for the run's own lines. */
|
||||
file: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type SyncState = {
|
||||
running: boolean;
|
||||
total: number | null;
|
||||
@@ -43,6 +54,8 @@ export type SyncState = {
|
||||
pruned: number;
|
||||
errors: SyncError[];
|
||||
current: string[];
|
||||
/** Only the lines the server had not already sent — see the `since` note in the feed. */
|
||||
log: LogLine[];
|
||||
startedAt: string | null;
|
||||
finishedAt: string | null;
|
||||
fatal: string | null;
|
||||
@@ -75,12 +88,21 @@ const KEY = ['fofos-admin', 'media-sync'];
|
||||
type Shared = {
|
||||
state: SyncState | null;
|
||||
ledger: Ledger | null;
|
||||
/**
|
||||
* The transcript, ACCUMULATED — the server sends deltas and this is the whole of it.
|
||||
*
|
||||
* Kept here rather than in the modal so closing the modal does not throw away the run's history.
|
||||
* Somebody who opens it, watches, closes it and opens it again is watching one run, not two.
|
||||
*/
|
||||
log: LogLine[];
|
||||
/** Which run `log` belongs to, so a new one clears it. See the note where it is compared. */
|
||||
logRun: string | null;
|
||||
/** A start request is in flight — distinct from `state.running`, which is the SERVER's answer. */
|
||||
busy: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
const EMPTY: Shared = { state: null, ledger: null, busy: false, error: null };
|
||||
const EMPTY: Shared = { state: null, ledger: null, log: [], logRun: null, busy: false, error: null };
|
||||
|
||||
export type MediaSync = Shared & {
|
||||
/** A pass is under way. */
|
||||
@@ -147,10 +169,44 @@ export const useMediaSyncFeed = (): void => {
|
||||
const setRef = useRef(setShared);
|
||||
setRef.current = setShared;
|
||||
|
||||
// The latest value, for the poll to read WITHOUT being rebuilt when it changes — an interval whose
|
||||
// callback identity changed every line would be torn down and restarted on every line.
|
||||
const sharedRef = useRef(shared);
|
||||
sharedRef.current = shared;
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const held = sharedRef.current;
|
||||
const since = held.log.at(-1)?.n;
|
||||
|
||||
try {
|
||||
const state = await clientRef.current.get<SyncState>('/fofos-admin/_config/media/sync');
|
||||
setRef.current((was) => ({ ...was, state }));
|
||||
/*
|
||||
Ask only for what we do not have.
|
||||
|
||||
A long run's transcript is thousands of lines and this polls every second, so without `since`
|
||||
the same megabyte would arrive over and over. On the first poll of a run there is nothing held,
|
||||
so `since` is absent and the server sends the lot — which is what a modal opened mid-run wants.
|
||||
*/
|
||||
const state = await clientRef.current.get<SyncState>(
|
||||
since === undefined
|
||||
? '/fofos-admin/_config/media/sync'
|
||||
: `/fofos-admin/_config/media/sync?since=${since}`,
|
||||
);
|
||||
|
||||
setRef.current((was) => {
|
||||
/*
|
||||
A NEW run resets the transcript.
|
||||
|
||||
`n` restarts at 1 each run, so appending across a boundary would interleave two runs and put
|
||||
the numbers out of order. `startedAt` is what identifies a run — comparing it is cheaper and
|
||||
more honest than watching `running` go false then true, which misses a run that began and
|
||||
ended between two polls.
|
||||
*/
|
||||
const run = state.startedAt;
|
||||
const fresh = run !== was.logRun;
|
||||
const log = fresh ? state.log : [...was.log, ...state.log];
|
||||
|
||||
return { ...was, state, log, logRun: run };
|
||||
});
|
||||
} catch {
|
||||
// Owner-only. Root Location has already said so once if that is the reason.
|
||||
setRef.current((was) => ({ ...was, state: null }));
|
||||
|
||||
Reference in New Issue
Block a user