mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-17 00:18:41 -07:00
fix: apply CodeRabbit auto-fixes
This commit is contained in:
@@ -2,5 +2,5 @@ type: fixed
|
|||||||
area: stats
|
area: stats
|
||||||
|
|
||||||
- Fixed Vocabulary totals and charts counting only the first browsing page instead of all tracked vocabulary, without delaying the rest of the page.
|
- Fixed Vocabulary totals and charts counting only the first browsing page instead of all tracked vocabulary, without delaying the rest of the page.
|
||||||
- New-word history now uses permanent daily lexical rollups, backfilled in the background and repaired when tracked material is removed or reprocessed.
|
- New-word history now uses permanent daily lexical rollups, backfilled in the background and repaired when tracked material is removed or reprocessed; playback writes queue safely during the one-time rebuild and resume afterward.
|
||||||
- Calendar-day chart labels now preserve the recorded local date in time zones west of UTC.
|
- Calendar-day chart labels now preserve the recorded local date in time zones west of UTC.
|
||||||
|
|||||||
@@ -609,6 +609,99 @@ test('tracker starts the injected lexical rollup backfill when it is pending', a
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tracker queues playback writes until lexical rollup backfill settles', async () => {
|
||||||
|
const dbPath = makeDbPath();
|
||||||
|
let tracker: ImmersionTrackerService | null = null;
|
||||||
|
let startBackfill = (): void => {};
|
||||||
|
let releaseBackfill = (): void => {};
|
||||||
|
let markBackfillStarted = (): void => {};
|
||||||
|
const backfillStartGate = new Promise<void>((resolve) => {
|
||||||
|
startBackfill = resolve;
|
||||||
|
});
|
||||||
|
const heldBackfill = new Promise<void>((resolve) => {
|
||||||
|
releaseBackfill = resolve;
|
||||||
|
});
|
||||||
|
const backfillStarted = new Promise<void>((resolve) => {
|
||||||
|
markBackfillStarted = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const setupDb = new Database(dbPath);
|
||||||
|
const { ensureSchema } = await import('./immersion-tracker/storage');
|
||||||
|
ensureSchema(setupDb);
|
||||||
|
setupDb
|
||||||
|
.prepare(
|
||||||
|
`UPDATE imm_rollup_state SET state_value = '0' WHERE state_key = 'lexical_daily_rollups_ready'`,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
setupDb.close();
|
||||||
|
|
||||||
|
const Ctor = await loadTrackerCtor();
|
||||||
|
tracker = new Ctor(
|
||||||
|
{ dbPath },
|
||||||
|
{
|
||||||
|
runLexicalRollupBackfillTask: async (workerDbPath) => {
|
||||||
|
await backfillStartGate;
|
||||||
|
const workerDb = new Database(workerDbPath);
|
||||||
|
try {
|
||||||
|
workerDb.exec('BEGIN IMMEDIATE');
|
||||||
|
markBackfillStarted();
|
||||||
|
await heldBackfill;
|
||||||
|
workerDb.exec('COMMIT');
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
workerDb.exec('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
// Preserve the original worker failure.
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
workerDb.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
tracker.handleMediaChange('https://example.com/backfill-test.mp4', 'Backfill Test');
|
||||||
|
startBackfill();
|
||||||
|
await backfillStarted;
|
||||||
|
tracker.recordCardsMined(1);
|
||||||
|
|
||||||
|
const privateApi = tracker as unknown as {
|
||||||
|
db: DatabaseSync;
|
||||||
|
queue: unknown[];
|
||||||
|
flushNow: () => void;
|
||||||
|
writeLock: { locked: boolean };
|
||||||
|
};
|
||||||
|
assert.equal(privateApi.writeLock.locked, true);
|
||||||
|
privateApi.flushNow();
|
||||||
|
|
||||||
|
assert.ok(privateApi.queue.length > 0);
|
||||||
|
assert.equal(
|
||||||
|
(
|
||||||
|
privateApi.db.prepare('SELECT COUNT(*) AS total FROM imm_session_events').get() as {
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
).total,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
releaseBackfill();
|
||||||
|
await waitForCondition(() => privateApi.queue.length === 0);
|
||||||
|
assert.equal(
|
||||||
|
(
|
||||||
|
privateApi.db.prepare('SELECT COUNT(*) AS total FROM imm_session_events').get() as {
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
).total,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
releaseBackfill();
|
||||||
|
tracker?.destroy();
|
||||||
|
cleanupDbPath(dbPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('startup backfills lifetime summaries when retained sessions exist but summary tables are empty', async () => {
|
test('startup backfills lifetime summaries when retained sessions exist but summary tables are empty', async () => {
|
||||||
const dbPath = makeDbPath();
|
const dbPath = makeDbPath();
|
||||||
let tracker: ImmersionTrackerService | null = null;
|
let tracker: ImmersionTrackerService | null = null;
|
||||||
|
|||||||
@@ -413,7 +413,10 @@ export class ImmersionTrackerService {
|
|||||||
private readonly monthlyRollupRetentionMs: number;
|
private readonly monthlyRollupRetentionMs: number;
|
||||||
private readonly vacuumIntervalMs: number;
|
private readonly vacuumIntervalMs: number;
|
||||||
private readonly dbPath: string;
|
private readonly dbPath: string;
|
||||||
private readonly writeLock = { locked: false };
|
private readonly writeLock = {
|
||||||
|
locked: false,
|
||||||
|
reasons: new Set<'flush' | 'delete-maintenance' | 'lexical-rollup-backfill'>(),
|
||||||
|
};
|
||||||
private readonly destroyDeleteMaintenanceRunner: () => void;
|
private readonly destroyDeleteMaintenanceRunner: () => void;
|
||||||
private readonly runVocabularySummaryTask: (
|
private readonly runVocabularySummaryTask: (
|
||||||
knownWords: ReadonlySet<string> | null,
|
knownWords: ReadonlySet<string> | null,
|
||||||
@@ -471,10 +474,10 @@ export class ImmersionTrackerService {
|
|||||||
runTask: (task) => runDeleteMaintenanceTask(this.dbPath, task),
|
runTask: (task) => runDeleteMaintenanceTask(this.dbPath, task),
|
||||||
onBusy: () => {
|
onBusy: () => {
|
||||||
this.requireWriteQueueDrained('delete maintenance');
|
this.requireWriteQueueDrained('delete maintenance');
|
||||||
this.writeLock.locked = true;
|
this.setWriteLock('delete-maintenance', true);
|
||||||
},
|
},
|
||||||
onIdle: () => {
|
onIdle: () => {
|
||||||
this.writeLock.locked = false;
|
this.setWriteLock('delete-maintenance', false);
|
||||||
if (!this.isDestroyed && this.queue.length > 0) this.scheduleFlush(0);
|
if (!this.isDestroyed && this.queue.length > 0) this.scheduleFlush(0);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -558,14 +561,6 @@ export class ImmersionTrackerService {
|
|||||||
this.db = new Database(this.dbPath);
|
this.db = new Database(this.dbPath);
|
||||||
applyPragmas(this.db);
|
applyPragmas(this.db);
|
||||||
ensureSchema(this.db);
|
ensureSchema(this.db);
|
||||||
if (!areLexicalDailyRollupsReady(this.db)) {
|
|
||||||
void this.runLexicalRollupBackfillTask().catch((error: unknown) => {
|
|
||||||
this.logger.warn(
|
|
||||||
'Lexical daily rollup backfill failed; it will retry on next startup',
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const reconciledSessions = reconcileStaleActiveSessions(this.db);
|
const reconciledSessions = reconcileStaleActiveSessions(this.db);
|
||||||
if (reconciledSessions > 0) {
|
if (reconciledSessions > 0) {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
@@ -594,6 +589,7 @@ export class ImmersionTrackerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.preparedStatements = createTrackerPreparedStatements(this.db);
|
this.preparedStatements = createTrackerPreparedStatements(this.db);
|
||||||
|
if (!areLexicalDailyRollupsReady(this.db)) this.startLexicalRollupBackfill();
|
||||||
this.scheduleMaintenance();
|
this.scheduleMaintenance();
|
||||||
this.scheduleFlush();
|
this.scheduleFlush();
|
||||||
}
|
}
|
||||||
@@ -967,6 +963,31 @@ export class ImmersionTrackerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setWriteLock(
|
||||||
|
reason: 'flush' | 'delete-maintenance' | 'lexical-rollup-backfill',
|
||||||
|
active: boolean,
|
||||||
|
): void {
|
||||||
|
if (active) this.writeLock.reasons.add(reason);
|
||||||
|
else this.writeLock.reasons.delete(reason);
|
||||||
|
this.writeLock.locked = this.writeLock.reasons.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private startLexicalRollupBackfill(): void {
|
||||||
|
this.requireWriteQueueDrained('lexical rollup backfill');
|
||||||
|
this.setWriteLock('lexical-rollup-backfill', true);
|
||||||
|
void this.runLexicalRollupBackfillTask()
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
this.logger.warn(
|
||||||
|
'Lexical daily rollup backfill failed; it will retry on next startup',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setWriteLock('lexical-rollup-backfill', false);
|
||||||
|
if (!this.isDestroyed && this.queue.length > 0) this.scheduleFlush(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async reassignAnimeAnilist(
|
async reassignAnimeAnilist(
|
||||||
animeId: number,
|
animeId: number,
|
||||||
info: {
|
info: {
|
||||||
@@ -2022,7 +2043,7 @@ export class ImmersionTrackerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const batch = this.queue.splice(0, Math.min(this.batchSize, this.queue.length));
|
const batch = this.queue.splice(0, Math.min(this.batchSize, this.queue.length));
|
||||||
this.writeLock.locked = true;
|
this.setWriteLock('flush', true);
|
||||||
try {
|
try {
|
||||||
this.db.exec('BEGIN IMMEDIATE');
|
this.db.exec('BEGIN IMMEDIATE');
|
||||||
for (const write of batch) {
|
for (const write of batch) {
|
||||||
@@ -2034,7 +2055,7 @@ export class ImmersionTrackerService {
|
|||||||
this.queue.unshift(...batch);
|
this.queue.unshift(...batch);
|
||||||
this.logger.warn('Immersion tracker flush failed, retrying later', error as Error);
|
this.logger.warn('Immersion tracker flush failed, retrying later', error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
this.writeLock.locked = false;
|
this.setWriteLock('flush', false);
|
||||||
this.flushScheduled = false;
|
this.flushScheduled = false;
|
||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
this.scheduleFlush(this.flushIntervalMs);
|
this.scheduleFlush(this.flushIntervalMs);
|
||||||
|
|||||||
Reference in New Issue
Block a user