commit 17a2d7be14e2645b9beb79b6e47ac7a2c551277b truex <truex@equwal.com> 2026-09-26 23:41:10 -0700 Cut off the OCR start with its own error, not AbortSignal.reason abort(reason) and AbortSignal.reason came in Chrome 98, and the app runs on WebView 91 and later (the SIMD core of tesseract.js). On an older WebView, a start that took too long rejected with undefined, not with OcrStartError. The strip then said "40 pages could not be read." in place of "OCR could not start". createOcrJob now rejects the start with the error that it keeps, and also aborts the signal of the start with it. A test makes abort() drop its reason, as an older WebView does.
src/ocr-job.ts | 30 +++++++++++++++++------------- test/ocr-job.test.ts | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-)
diff --git a/src/ocr-job.ts b/src/ocr-job.ts index 929f21c..9cc8f3c 100644 --- a/src/ocr-job.ts +++ b/src/ocr-job.ts @@ -88,27 +88,31 @@ export function createOcrJob<Image>( startLimitMs = START_LIMIT_MS, ): OcrJob<Image> { let running: Promise<OcrWorker<Image>> | undefined; - /** The control of the start that runs now, if any. */ - let starting: AbortController | undefined; + /** Cuts off the start that runs now, if any. */ + let cutStart: ((reason: Error) => void) | undefined; let stopped = false; function startInTime(): Promise<OcrWorker<Image>> { const control = new AbortController(); - starting = control; const started = startWorker(control.signal); - let timer: ReturnType<typeof setTimeout> | undefined; + // The start rejects with the reason itself: AbortSignal.reason needs + // WebView 98, and the app runs on WebView 91 and later. + let cutOff: (reason: Error) => void = () => undefined; const cut = new Promise<never>((_resolve, reject) => { - control.signal.addEventListener('abort', () => reject(control.signal.reason), { - once: true, - }); - timer = setTimeout(() => { - const s = Math.round(startLimitMs / 1000); - control.abort(new OcrStartError(new Error(`The start took more than ${s} s.`))); - }, startLimitMs); + cutOff = (reason) => { + reject(reason); + control.abort(reason); + }; }); + cutStart = cutOff; + const s = Math.round(startLimitMs / 1000); + const timer = setTimeout( + () => cutOff(new OcrStartError(new Error(`The start took more than ${s} s.`))), + startLimitMs, + ); const end = (): void => { clearTimeout(timer); - if (starting === control) starting = undefined; + if (cutStart === cutOff) cutStart = undefined; }; return Promise.race([started, cut]).then( (ocr) => { @@ -156,7 +160,7 @@ export function createOcrJob<Image>( }, stop() { stopped = true; - starting?.abort(new Error('OCR stopped.')); + cutStart?.(new Error('OCR stopped.')); stopOcr(running); running = undefined; }, diff --git a/test/ocr-job.test.ts b/test/ocr-job.test.ts index 750b6b8..edbcecc 100644 --- a/test/ocr-job.test.ts +++ b/test/ocr-job.test.ts @@ -209,6 +209,26 @@ describe('createOcrJob', () => { expect(vi.getTimerCount()).toBe(0); }); + it('gives the start error also where abort() keeps no reason (WebView before 98)', async () => { + // abort(reason) and AbortSignal.reason came in Chrome 98. The SIMD core of + // the app runs from WebView 91. + const abort = AbortController.prototype.abort; + const noReason = vi.spyOn(AbortController.prototype, 'abort').mockImplementation(function ( + this: AbortController, + ) { + abort.call(this); + }); + try { + const { start } = stalledStart(); + const job = createOcrJob(start, 1000); + const started = job.start().catch((e: unknown) => e); + await vi.advanceTimersByTimeAsync(1000); + expect(await started).toBeInstanceOf(OcrStartError); + } finally { + noReason.mockRestore(); + } + }); + it('stops a worker that starts after the start limit', async () => { const worker = fakeWorker(readPage); const start = vi.fn(