timing
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
isAwaitingReview,
|
||||
isDismissed,
|
||||
isSettled,
|
||||
pollDelayFor,
|
||||
currentStage,
|
||||
isStuckOnMissingRunner,
|
||||
productsOf,
|
||||
@@ -334,3 +335,36 @@ test('the run id is on the drop, and gone from the run it points to', () => {
|
||||
// cannot be written using the run's.
|
||||
assert.notEqual(drop.batch_id, run.batch_id);
|
||||
});
|
||||
|
||||
/* ── How often to look, and when to stop looking ──────────────────────────── */
|
||||
|
||||
/*
|
||||
The console used to stop polling the moment a drop went to review, on the
|
||||
reasoning that waiting for an admin is not progress. It is not — but the release
|
||||
IS, and stopping there meant the panel said "waiting for review" until somebody
|
||||
reloaded the page. A step-by-step panel that only advances on reload is the
|
||||
thing the panel exists to replace.
|
||||
*/
|
||||
|
||||
test('a review hold is polled slowly, not abandoned', () => {
|
||||
assert.equal(isAwaitingReview(held), true);
|
||||
assert.equal(pollDelayFor(held), 15000, 'a hold can last hours; 2s would be 1,800 reads an hour');
|
||||
});
|
||||
|
||||
test('a running batch is polled at a pace a person can watch', () => {
|
||||
const running = { ...held, status: 'running' } satisfies IngestBatch;
|
||||
assert.equal(isAwaitingReview(running), false);
|
||||
assert.equal(pollDelayFor(running), 2000);
|
||||
});
|
||||
|
||||
// A released drop is no longer waiting on anybody, so it goes back to the fast
|
||||
// cadence even though its own status still reads "pending".
|
||||
test('a released drop is followed at the running pace', () => {
|
||||
const released = {
|
||||
...held,
|
||||
files: [{ index: 0, filename: 'qa.csv', status: 'queued' as const, released_to: 'run-77' }],
|
||||
} satisfies IngestBatch;
|
||||
assert.equal(releasedRunId(released), 'run-77');
|
||||
assert.equal(isAwaitingReview(released), false);
|
||||
assert.equal(pollDelayFor(released), 2000);
|
||||
});
|
||||
|
||||
@@ -518,13 +518,72 @@ export function isSettled(batch: IngestBatch): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Sleeps, unless the caller aborts first. */
|
||||
function wait(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(finish, ms);
|
||||
function finish() {
|
||||
clearTimeout(timer);
|
||||
signal?.removeEventListener('abort', finish);
|
||||
resolve();
|
||||
}
|
||||
signal?.addEventListener('abort', finish, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolves the moment the tab is visible again — immediately if it already is. */
|
||||
function whenVisible(signal?: AbortSignal): Promise<void> {
|
||||
if (typeof document === 'undefined' || document.visibilityState === 'visible') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const finish = () => {
|
||||
if (document.visibilityState !== 'visible' && !signal?.aborted) return;
|
||||
document.removeEventListener('visibilitychange', finish);
|
||||
signal?.removeEventListener('abort', finish);
|
||||
resolve();
|
||||
};
|
||||
document.addEventListener('visibilitychange', finish);
|
||||
signal?.addEventListener('abort', finish, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
/** How long to wait before the next reading, given where the batch has got to. */
|
||||
export function pollDelayFor(batch: IngestBatch): number {
|
||||
// A run is minutes and a person is watching the stage name move.
|
||||
if (!isAwaitingReview(batch)) return 2000;
|
||||
// A review hold is however long their admin takes — sometimes hours. Two
|
||||
// seconds against that is 1,800 requests an hour to be told "still waiting".
|
||||
return 15000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls until the batch settles.
|
||||
* Polls until the batch is finished, THROUGH the review hold.
|
||||
*
|
||||
* Every two seconds. The pipeline's own stages take far longer than that, and a
|
||||
* person is watching a progress bar — a slower cadence buys nothing but a
|
||||
* screen that looks stuck. `onTick` fires on each reading so the caller can
|
||||
* render the stage name and row counts as they move.
|
||||
* ── Why it no longer stops at "awaiting review" ─────────────────────────────
|
||||
*
|
||||
* It used to return there, on the reasoning that waiting for an admin is not
|
||||
* progress. True, but it left the console showing "waiting for review" forever
|
||||
* once that admin released the drop — the steps only moved when the operator
|
||||
* reloaded the page, which is the one thing a step-by-step progress panel is
|
||||
* supposed to save them from. The release is exactly the transition worth
|
||||
* watching: it is when the drop becomes a run and the products start arriving.
|
||||
*
|
||||
* So the hold is polled too, at `pollDelayFor`'s slower cadence — 15s rather
|
||||
* than 2s, because a hold can last hours and a person is not watching a bar
|
||||
* during one.
|
||||
*
|
||||
* ── And why a hidden tab costs nothing ──────────────────────────────────────
|
||||
*
|
||||
* Polling pauses entirely while the tab is in the background and takes a
|
||||
* reading the instant it comes forward. So a sheet left open in another tab all
|
||||
* afternoon makes no requests, and is up to date by the time the operator has
|
||||
* looked at it — which is the same thing they used to get from reloading, minus
|
||||
* the reload.
|
||||
*
|
||||
* `onTick` fires on each reading so the caller renders stage names and row
|
||||
* counts as they move. It stops on a result and on a dismissal: a declined drop
|
||||
* will never produce one.
|
||||
*/
|
||||
export async function pollBatch(
|
||||
batchId: string,
|
||||
@@ -539,13 +598,10 @@ export async function pollBatch(
|
||||
// "released" forever.
|
||||
const batch = await resolveBatch(await fetchBatch(batchId, signal), signal);
|
||||
onTick(batch);
|
||||
// Stops on a review hold and on a dismissal as well as on a result. Waiting
|
||||
// for an admin is not progress, a declined drop will never produce one, and
|
||||
// a browser tab cannot outlast either — the drop id is what the operator
|
||||
// comes back with.
|
||||
if (isSettled(batch) || isAwaitingReview(batch) || isDismissed(batch)) return batch;
|
||||
if (isSettled(batch) || isDismissed(batch)) return batch;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await wait(pollDelayFor(batch), signal);
|
||||
await whenVisible(signal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user