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.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 path from 'node:path';
|
||||
import { createLogger } from '../../../logger';
|
||||
import { executeLexicalRollupBackfillTask } from './lexical-rollup-worker';
|
||||
|
||||
interface WorkerResponse {
|
||||
ok?: boolean;
|
||||
@@ -15,6 +14,12 @@ interface WorkerHandle {
|
||||
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');
|
||||
|
||||
export function resolveLexicalRollupWorkerPath(): string | null {
|
||||
@@ -29,21 +34,27 @@ export class LexicalRollupWorkerRuntime {
|
||||
private readonly activeWorkers = new Set<WorkerHandle>();
|
||||
private destroyed = false;
|
||||
|
||||
constructor(private readonly options: LexicalRollupWorkerRuntimeOptions = {}) {}
|
||||
|
||||
async run(dbPath: string): Promise<void> {
|
||||
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
||||
let worker: WorkerHandle;
|
||||
try {
|
||||
const workerPath = resolveLexicalRollupWorkerPath();
|
||||
const workerPath = (this.options.resolveWorkerPath ?? resolveLexicalRollupWorkerPath)();
|
||||
if (!workerPath) throw new Error('Emitted lexical rollup worker module was not found');
|
||||
const { Worker } = await import('node:worker_threads');
|
||||
worker = new Worker(workerPath, { workerData: { dbPath } });
|
||||
const createWorker =
|
||||
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) {
|
||||
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
||||
logger.warn(
|
||||
'Lexical rollup worker unavailable; running backfill on the current thread',
|
||||
(this.options.warn ?? logger.warn)(
|
||||
'Lexical rollup worker unavailable; leaving backfill pending for a later startup',
|
||||
error,
|
||||
);
|
||||
executeLexicalRollupBackfillTask(dbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const LOCAL_EPOCH_DAY_SQL = `
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
export function rebuildLexicalDailyRollups(db: DatabaseSync): void {
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
let transactionStarted = false;
|
||||
try {
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
transactionStarted = true;
|
||||
db.exec('DELETE FROM imm_lexical_daily_rollups');
|
||||
db.exec(`
|
||||
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);
|
||||
db.exec('COMMIT');
|
||||
} catch (error) {
|
||||
db.exec('ROLLBACK');
|
||||
if (transactionStarted) db.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ import type {
|
||||
} from './types';
|
||||
import { fromDbTimestamp, toDbTimestamp } from './query-shared';
|
||||
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_MIN = 100;
|
||||
@@ -185,7 +189,7 @@ export function getVocabularyChartData(db: DatabaseSync): VocabularyChartData {
|
||||
.prepare(
|
||||
`
|
||||
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
|
||||
WHERE headword IN (${placeholders}) OR word IN (${placeholders}) OR reading IN (${placeholders})
|
||||
`,
|
||||
|
||||
@@ -662,6 +662,8 @@ function buildNewWordsPerDay(
|
||||
axis: number[] | null,
|
||||
): TrendChartPoint[] {
|
||||
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 rows = getLexicalDailyRollups(db).filter(
|
||||
(row) => cutoffDay === null || row.epochDay >= cutoffDay,
|
||||
|
||||
Reference in New Issue
Block a user