mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-18 12:18:29 -07:00
fix(stats): version lexical rollups and preserve backfill writes
- Persist vocabulary_visible on imm_words and filter rollups/triggers by it, matching the totals' exclusion rules - Normalize legacy second/millisecond timestamps when bucketing rollup rows into local days - Replace the boolean rollup-ready flag with a version key so schema changes trigger an atomic rebuild - Preserve queued playback writes during rollup backfill instead of dropping them under queue pressure - Time out the lexical rollup worker instead of hanging forever on no response - Key concurrent vocabulary summary requests by their known-words snapshot so they no longer share stale state - Bound stats dashboard retries for unfinished vocabulary loads before showing the inline Retry control
This commit is contained in:
@@ -285,6 +285,36 @@ test('charts poll while the backfill is pending and stop once it is ready', asyn
|
||||
}
|
||||
});
|
||||
|
||||
test('chart backfill polling stops and surfaces Retry when readiness never arrives', async () => {
|
||||
let chartCalls = 0;
|
||||
const restoreClient = stubVocabularyClient({
|
||||
getVocabularySummary: async () => summaryFixture(),
|
||||
getVocabularyCharts: async () => {
|
||||
chartCalls += 1;
|
||||
return chartsFixture({ ready: false });
|
||||
},
|
||||
});
|
||||
const harness = await mountHook();
|
||||
|
||||
try {
|
||||
await harness.flush();
|
||||
for (let poll = 0; poll < 65; poll += 1) await harness.tick(5_000);
|
||||
|
||||
assert.equal(chartCalls, 60, 'a failed backfill must not poll for the lifetime of the tab');
|
||||
assert.match(harness.state().aggregatesError ?? '', /still building/i);
|
||||
|
||||
await act(async () => {
|
||||
harness.state().refreshAggregates();
|
||||
});
|
||||
await harness.flush();
|
||||
assert.equal(chartCalls, 61, 'Retry starts one fresh bounded polling cycle');
|
||||
assert.equal(harness.state().aggregatesError, null);
|
||||
} finally {
|
||||
await harness.teardown();
|
||||
restoreClient();
|
||||
}
|
||||
});
|
||||
|
||||
test('aggregates refetch after an exclusion edit is acknowledged by the server', async () => {
|
||||
let summaryCalls = 0;
|
||||
let chartCalls = 0;
|
||||
|
||||
@@ -14,6 +14,7 @@ const AGGREGATE_RETRY_LIMIT = 5;
|
||||
const CHART_BACKFILL_POLL_MS = 1_000;
|
||||
const CHART_BACKFILL_SLOW_POLL_MS = 5_000;
|
||||
const CHART_BACKFILL_FAST_POLLS = 30;
|
||||
const CHART_BACKFILL_POLL_LIMIT = 60;
|
||||
|
||||
function aggregateRetryDelayMs(attempt: number): number {
|
||||
return Math.min(AGGREGATE_RETRY_BASE_MS * 2 ** attempt, AGGREGATE_RETRY_MAX_MS);
|
||||
@@ -117,12 +118,20 @@ export function useVocabulary() {
|
||||
if (cancelled) return;
|
||||
setCharts(nextCharts);
|
||||
if (!nextCharts.ready) {
|
||||
schedule(
|
||||
() => loadCharts(0, readyPolls + 1),
|
||||
readyPolls < CHART_BACKFILL_FAST_POLLS
|
||||
? CHART_BACKFILL_POLL_MS
|
||||
: CHART_BACKFILL_SLOW_POLL_MS,
|
||||
);
|
||||
const completedPolls = readyPolls + 1;
|
||||
if (completedPolls < CHART_BACKFILL_POLL_LIMIT) {
|
||||
schedule(
|
||||
() => loadCharts(0, completedPolls),
|
||||
completedPolls < CHART_BACKFILL_FAST_POLLS
|
||||
? CHART_BACKFILL_POLL_MS
|
||||
: CHART_BACKFILL_SLOW_POLL_MS,
|
||||
);
|
||||
} else {
|
||||
setAggregatesError(
|
||||
(previous) =>
|
||||
previous ?? 'Vocabulary charts are still building. Retry to check again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((chartError: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user