Each polling page repeated its own fetch-and-set code on a fixed timer, so a slow answer could be overtaken by the next tick, an older response could overwrite a newer one, and nothing aborted a request when the page went away. usePolled skips a tick while a request is in flight, aborts on unmount or refresh, ignores answers from dropped requests, and keeps the last good data under an error. The alerts, hives, agents, schedules, builds, review runs and broadcast pages and the subagent list use it.
94 lines
2.8 KiB
JavaScript
94 lines
2.8 KiB
JavaScript
// The request discipline of a polled page. Run by `npm test`, which Node runs
|
|
// with its built-in test runner and type stripping, so the `.ts` module is
|
|
// imported as it is.
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { createPoller } from "../src/ui/polled/poller.ts";
|
|
|
|
// A fetch whose answers the test settles by hand, in any order.
|
|
function manualFetch() {
|
|
const calls = [];
|
|
const fetch = (signal) =>
|
|
new Promise((resolve, reject) => {
|
|
calls.push({ signal, resolve, reject });
|
|
});
|
|
return { calls, fetch };
|
|
}
|
|
|
|
// Lets the promise callbacks of a settled request run.
|
|
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
|
|
|
function collect() {
|
|
const seen = { data: [], error: [] };
|
|
return {
|
|
seen,
|
|
onData: (d) => seen.data.push(d),
|
|
onError: (e) => seen.error.push(e),
|
|
};
|
|
}
|
|
|
|
test("a tick while a request is in flight asks for nothing", async () => {
|
|
const { calls, fetch } = manualFetch();
|
|
const sink = collect();
|
|
const poller = createPoller({ fetch, ...sink });
|
|
poller.tick();
|
|
poller.tick();
|
|
assert.equal(calls.length, 1);
|
|
calls[0].resolve("one");
|
|
await flush();
|
|
assert.deepEqual(sink.seen.data, ["one"]);
|
|
poller.tick();
|
|
assert.equal(calls.length, 2);
|
|
});
|
|
|
|
test("a refetch drops the request in flight, so an older answer never lands", async () => {
|
|
const { calls, fetch } = manualFetch();
|
|
const sink = collect();
|
|
const poller = createPoller({ fetch, ...sink });
|
|
poller.tick();
|
|
poller.refetch();
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(calls[0].signal.aborted, true);
|
|
calls[1].resolve("new");
|
|
calls[0].resolve("old");
|
|
await flush();
|
|
assert.deepEqual(sink.seen.data, ["new"]);
|
|
});
|
|
|
|
test("a stopped poller aborts the request and ignores its answer", async () => {
|
|
const { calls, fetch } = manualFetch();
|
|
const sink = collect();
|
|
const poller = createPoller({ fetch, ...sink });
|
|
poller.tick();
|
|
poller.stop();
|
|
assert.equal(calls[0].signal.aborted, true);
|
|
calls[0].resolve("late");
|
|
await flush();
|
|
assert.deepEqual(sink.seen.data, []);
|
|
poller.tick();
|
|
assert.equal(calls.length, 2);
|
|
});
|
|
|
|
test("a failed request reports the error and frees the next tick", async () => {
|
|
const { calls, fetch } = manualFetch();
|
|
const sink = collect();
|
|
const poller = createPoller({ fetch, ...sink });
|
|
poller.tick();
|
|
const boom = new Error("boom");
|
|
calls[0].reject(boom);
|
|
await flush();
|
|
assert.deepEqual(sink.seen.error, [boom]);
|
|
poller.tick();
|
|
assert.equal(calls.length, 2);
|
|
});
|
|
|
|
test("the answer of a dropped request that fails is not reported", async () => {
|
|
const { calls, fetch } = manualFetch();
|
|
const sink = collect();
|
|
const poller = createPoller({ fetch, ...sink });
|
|
poller.tick();
|
|
poller.refetch();
|
|
calls[0].reject(new Error("aborted"));
|
|
await flush();
|
|
assert.deepEqual(sink.seen.error, []);
|
|
});
|