mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-16 01:55:51 -07:00
fix(stats): keep lexical rollup backfill async
This commit is contained in:
@@ -54,3 +54,16 @@ test('lexical rollup worker module resolves in the current layout', () => {
|
|||||||
assert.ok(workerPath, 'expected the lexical rollup worker module to resolve');
|
assert.ok(workerPath, 'expected the lexical rollup worker module to resolve');
|
||||||
assert.ok(workerPath.endsWith(__filename.endsWith('.ts') ? '.ts' : '.js'));
|
assert.ok(workerPath.endsWith(__filename.endsWith('.ts') ? '.ts' : '.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('lexical rollup worker leaves a backfill pending when no worker can start', async () => {
|
||||||
|
const runtime = new LexicalRollupWorkerRuntime({
|
||||||
|
resolveWorkerPath: () => null,
|
||||||
|
warn: () => {},
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.doesNotReject(runtime.run('/tmp/not-used.sqlite'));
|
||||||
|
} finally {
|
||||||
|
runtime.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { createLogger } from '../../../logger';
|
import { createLogger } from '../../../logger';
|
||||||
import { executeLexicalRollupBackfillTask } from './lexical-rollup-worker';
|
|
||||||
|
|
||||||
interface WorkerResponse {
|
interface WorkerResponse {
|
||||||
ok?: boolean;
|
ok?: boolean;
|
||||||
@@ -15,6 +14,12 @@ interface WorkerHandle {
|
|||||||
terminate(): Promise<number>;
|
terminate(): Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LexicalRollupWorkerRuntimeOptions {
|
||||||
|
resolveWorkerPath?: () => string | null;
|
||||||
|
createWorker?: (workerPath: string, workerData: { dbPath: string }) => Promise<WorkerHandle>;
|
||||||
|
warn?: (message: string, ...meta: unknown[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const logger = createLogger('main:immersion-tracker:lexical-rollup-worker');
|
const logger = createLogger('main:immersion-tracker:lexical-rollup-worker');
|
||||||
|
|
||||||
export function resolveLexicalRollupWorkerPath(): string | null {
|
export function resolveLexicalRollupWorkerPath(): string | null {
|
||||||
@@ -29,21 +34,27 @@ export class LexicalRollupWorkerRuntime {
|
|||||||
private readonly activeWorkers = new Set<WorkerHandle>();
|
private readonly activeWorkers = new Set<WorkerHandle>();
|
||||||
private destroyed = false;
|
private destroyed = false;
|
||||||
|
|
||||||
|
constructor(private readonly options: LexicalRollupWorkerRuntimeOptions = {}) {}
|
||||||
|
|
||||||
async run(dbPath: string): Promise<void> {
|
async run(dbPath: string): Promise<void> {
|
||||||
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
||||||
let worker: WorkerHandle;
|
let worker: WorkerHandle;
|
||||||
try {
|
try {
|
||||||
const workerPath = resolveLexicalRollupWorkerPath();
|
const workerPath = (this.options.resolveWorkerPath ?? resolveLexicalRollupWorkerPath)();
|
||||||
if (!workerPath) throw new Error('Emitted lexical rollup worker module was not found');
|
if (!workerPath) throw new Error('Emitted lexical rollup worker module was not found');
|
||||||
const { Worker } = await import('node:worker_threads');
|
const createWorker =
|
||||||
worker = new Worker(workerPath, { workerData: { dbPath } });
|
this.options.createWorker ??
|
||||||
|
(async (resolvedPath, workerData) => {
|
||||||
|
const { Worker } = await import('node:worker_threads');
|
||||||
|
return new Worker(resolvedPath, { workerData });
|
||||||
|
});
|
||||||
|
worker = await createWorker(workerPath, { dbPath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
||||||
logger.warn(
|
(this.options.warn ?? logger.warn)(
|
||||||
'Lexical rollup worker unavailable; running backfill on the current thread',
|
'Lexical rollup worker unavailable; leaving backfill pending for a later startup',
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
executeLexicalRollupBackfillTask(dbPath);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const LOCAL_EPOCH_DAY_SQL = `
|
|||||||
CAST(julianday(CAST(%VALUE% AS REAL), 'unixepoch', 'localtime') - 2440587.5 AS INTEGER)
|
CAST(julianday(CAST(%VALUE% AS REAL), 'unixepoch', 'localtime') - 2440587.5 AS INTEGER)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function localEpochDaySql(value: string): string {
|
export function localEpochDaySql(value: string): string {
|
||||||
return LOCAL_EPOCH_DAY_SQL.replace('%VALUE%', value);
|
return LOCAL_EPOCH_DAY_SQL.replace('%VALUE%', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +131,10 @@ export function markLexicalDailyRollupsReady(db: DatabaseSync): void {
|
|||||||
|
|
||||||
/** Rebuild from the first-seen source of truth; run off the UI/main DB thread. */
|
/** Rebuild from the first-seen source of truth; run off the UI/main DB thread. */
|
||||||
export function rebuildLexicalDailyRollups(db: DatabaseSync): void {
|
export function rebuildLexicalDailyRollups(db: DatabaseSync): void {
|
||||||
db.exec('BEGIN IMMEDIATE');
|
let transactionStarted = false;
|
||||||
try {
|
try {
|
||||||
|
db.exec('BEGIN IMMEDIATE');
|
||||||
|
transactionStarted = true;
|
||||||
db.exec('DELETE FROM imm_lexical_daily_rollups');
|
db.exec('DELETE FROM imm_lexical_daily_rollups');
|
||||||
db.exec(`
|
db.exec(`
|
||||||
INSERT INTO imm_lexical_daily_rollups(epoch_day, word_count, word_count_without_names, kanji_count)
|
INSERT INTO imm_lexical_daily_rollups(epoch_day, word_count, word_count_without_names, kanji_count)
|
||||||
@@ -151,7 +153,7 @@ export function rebuildLexicalDailyRollups(db: DatabaseSync): void {
|
|||||||
markLexicalDailyRollupsReady(db);
|
markLexicalDailyRollupsReady(db);
|
||||||
db.exec('COMMIT');
|
db.exec('COMMIT');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
db.exec('ROLLBACK');
|
if (transactionStarted) db.exec('ROLLBACK');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ import type {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { fromDbTimestamp, toDbTimestamp } from './query-shared';
|
import { fromDbTimestamp, toDbTimestamp } from './query-shared';
|
||||||
import { nowMs } from './time';
|
import { nowMs } from './time';
|
||||||
import { areLexicalDailyRollupsReady, getLexicalDailyRollups } from './lexical-rollups';
|
import {
|
||||||
|
areLexicalDailyRollupsReady,
|
||||||
|
getLexicalDailyRollups,
|
||||||
|
localEpochDaySql,
|
||||||
|
} from './lexical-rollups';
|
||||||
|
|
||||||
const VOCABULARY_STATS_FILTER_OVERSAMPLE_FACTOR = 4;
|
const VOCABULARY_STATS_FILTER_OVERSAMPLE_FACTOR = 4;
|
||||||
const VOCABULARY_STATS_FILTER_OVERSAMPLE_MIN = 100;
|
const VOCABULARY_STATS_FILTER_OVERSAMPLE_MIN = 100;
|
||||||
@@ -185,7 +189,7 @@ export function getVocabularyChartData(db: DatabaseSync): VocabularyChartData {
|
|||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
SELECT headword, word, reading, pos2,
|
SELECT headword, word, reading, pos2,
|
||||||
CAST(julianday(CAST(first_seen AS REAL), 'unixepoch', 'localtime') - 2440587.5 AS INTEGER) AS epochDay
|
${localEpochDaySql('first_seen')} AS epochDay
|
||||||
FROM imm_words
|
FROM imm_words
|
||||||
WHERE headword IN (${placeholders}) OR word IN (${placeholders}) OR reading IN (${placeholders})
|
WHERE headword IN (${placeholders}) OR word IN (${placeholders}) OR reading IN (${placeholders})
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -662,6 +662,8 @@ function buildNewWordsPerDay(
|
|||||||
axis: number[] | null,
|
axis: number[] | null,
|
||||||
): TrendChartPoint[] {
|
): TrendChartPoint[] {
|
||||||
if (areLexicalDailyRollupsReady(db)) {
|
if (areLexicalDailyRollupsReady(db)) {
|
||||||
|
// A trend range is defined in calendar buckets, so the rollup includes the
|
||||||
|
// complete local cutoff day rather than applying a time-of-day boundary.
|
||||||
const cutoffDay = cutoffMs === null ? null : getLocalEpochDay(db, cutoffMs);
|
const cutoffDay = cutoffMs === null ? null : getLocalEpochDay(db, cutoffMs);
|
||||||
const rows = getLexicalDailyRollups(db).filter(
|
const rows = getLexicalDailyRollups(db).filter(
|
||||||
(row) => cutoffDay === null || row.epochDay >= cutoffDay,
|
(row) => cutoffDay === null || row.epochDay >= cutoffDay,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export function useVocabulary() {
|
|||||||
})
|
})
|
||||||
.catch((chartError: unknown) => {
|
.catch((chartError: unknown) => {
|
||||||
console.error('Failed to load vocabulary charts', chartError);
|
console.error('Failed to load vocabulary charts', chartError);
|
||||||
|
if (!cancelled) chartRetryTimer = setTimeout(loadCharts, 1_000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
loadCharts();
|
loadCharts();
|
||||||
|
|||||||
Reference in New Issue
Block a user