fix(subtitle-prefetch): reuse startup runs and prefetch overlap lines

- Warm combined text for overlapping subtitle cues.
- Preserve valid in-flight tokenization across startup restarts while rejecting invalidated cache results.
This commit is contained in:
sudacode committed 2026-09-28 01:17:59 -07:00
1 parent 4648c5e5a1
commit a4325740b7
9 files changed
+274 -6

No files matched your search

@@ -300,3 +300,65 @@ test('prefetch service deduplicates repeated cue text within a run', async () =>
);
assert.ok(tokenizedTexts.includes('other'));
});
test('prefetch service keeps an in-flight result from a superseded run', async () => {
const cues = makeCues(5);
const cachedTexts: string[] = [];
let releaseFirst!: () => void;
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const service = createSubtitlePrefetchService({
cues,
tokenizeSubtitle: async (text) => {
if (text === 'line-0') await firstGate;
return { text, tokens: [] };
},
preCacheTokenization: (text) => {
cachedTexts.push(text);
},
getCacheGeneration: () => 0,
priorityWindowSize: 3,
});
service.start(0);
await flushMicrotasks();
service.stop();
releaseFirst();
await flushMicrotasks();
assert.deepEqual(cachedTexts, ['line-0']);
});
test('prefetch service drops an in-flight result after cache invalidation', async () => {
const cues = makeCues(5);
const cachedTexts: string[] = [];
let generation = 0;
let releaseFirst!: () => void;
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const service = createSubtitlePrefetchService({
cues,
tokenizeSubtitle: async (text) => {
if (text === 'line-0') await firstGate;
return { text, tokens: [] };
},
preCacheTokenization: (text) => {
cachedTexts.push(text);
},
getCacheGeneration: () => generation,
priorityWindowSize: 3,
});
service.start(0);
await flushMicrotasks();
service.stop();
generation += 1;
releaseFirst();
await flushMicrotasks();
assert.deepEqual(cachedTexts, []);
});
+10 -1
View File
@@ -7,6 +7,12 @@ export interface SubtitlePrefetchServiceDeps {
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
preCacheTokenization: (text: string, data: SubtitleData) => void;
hasCachedTokenization?: (text: string) => boolean;
/**
* Bumped whenever the tokenization cache is invalidated. A result is cached only if
* the generation is unchanged since its tokenization began; without this dep every
* result counts as fresh.
*/
getCacheGeneration?: () => number;
priorityWindowSize?: number;
}
@@ -88,9 +94,12 @@ export function createSubtitlePrefetchService(
}
warmedKeys.add(cacheKey);
// Freshness follows the cache generation, not the run: a seek or restart that
// supersedes this run leaves the result valid, and the parser time is already spent.
const generation = deps.getCacheGeneration?.();
try {
const result = await deps.tokenizeSubtitle(cue.text);
if (result && !stopped && runId === currentRunId) {
if (result && generation === deps.getCacheGeneration?.()) {
deps.preCacheTokenization(cue.text, result);
}
} catch {
@@ -43,6 +43,8 @@ export interface SubtitleProcessingController {
*/
notePlainSubtitleEmitted: (text: string) => void;
invalidateTokenizationCache: () => void;
/** Incremented by invalidateTokenizationCache; lets async writers detect stale results. */
getCacheGeneration: () => number;
preCacheTokenization: (text: string, data: SubtitleData) => void;
consumeCachedSubtitle: (text: string) => SubtitleData | null;
hasCachedSubtitle: (text: string) => boolean;
@@ -253,6 +255,7 @@ export function createSubtitleProcessingController(
tokenizationCache.clear();
cacheGeneration += 1;
},
getCacheGeneration: () => cacheGeneration,
preCacheTokenization: (text: string, data: SubtitleData) => {
setCachedTokenization(text, data);
},