From 2b9d35cb79569e69c9acb3c8c18b768b7fa11b15 Mon Sep 17 00:00:00 2001 From: israel Date: Thu, 3 Sep 2026 10:15:50 +0100 Subject: [PATCH] fix(platform): shim the Map method pdfjs needs on Node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scanned PDFs index thin or empty because image detection never runs. pdfjs 5.x calls Map.prototype.getOrInsertComputed on its own Maps in both the main and worker builds. Node 22 does not ship it, so the operator-list read throws once per page — and that read is what finds embedded images and decides a page is scanned and needs OCR. Text extraction still runs, so a digital PDF looks fine while a scanned one indexes with almost nothing and only a warning to say so. The shim goes in ensureEs2025Shims beside the Promise.try and base64 codec fills, which already use the same typeof guard and defineProperty idiom for the same reason: lib.esnext declares the method, so no local type declaration is needed, and a real runtime implementation wins the guard. Also corrects the module docstring, which still described the Convex node action runtime that no longer exists. Closes #3018. Supersedes #3103, whose files #3125 deleted. --- .../extraction/pdfjs_dom_polyfill.test.ts | 61 +++++++++++++++++++ .../extraction/pdfjs_dom_polyfill.ts | 35 ++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts b/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts index 4e81fc46d3..3f029b6eac 100644 --- a/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts +++ b/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts @@ -61,3 +61,64 @@ describe('installPdfjsDomGlobals base64 codecs', () => { expect(new TextDecoder().decode(plaintext)).toBe('integration-secret'); }); }); + +describe('installPdfjsDomGlobals Map.getOrInsertComputed', () => { + it('installs the method pdfjs calls on its Maps', () => { + // Its absence is what broke the operator-list read per page (#3018). + installPdfjsDomGlobals(); + expect(typeof Map.prototype.getOrInsertComputed).toBe('function'); + }); + + it('computes and inserts when the key is absent', () => { + installPdfjsDomGlobals(); + const map = new Map(); + const calls: string[] = []; + const value = map.getOrInsertComputed('a', (key) => { + calls.push(key); + return 1; + }); + expect(value).toBe(1); + expect(map.get('a')).toBe(1); + expect(calls).toEqual(['a']); + }); + + it('returns the existing value without calling the callback', () => { + // pdfjs relies on this: the callback builds an intent state, and calling + // it twice would discard the one already in flight. + installPdfjsDomGlobals(); + const map = new Map([['a', 1]]); + let called = false; + const value = map.getOrInsertComputed('a', () => { + called = true; + return 2; + }); + expect(value).toBe(1); + expect(called).toBe(false); + }); + + it('stores a value the callback returns even when it is undefined', () => { + // Presence, not truthiness: a second call must not recompute. + installPdfjsDomGlobals(); + const map = new Map(); + map.getOrInsertComputed('a', () => undefined); + expect(map.has('a')).toBe(true); + let recomputed = false; + map.getOrInsertComputed('a', () => { + recomputed = true; + return undefined; + }); + expect(recomputed).toBe(false); + }); + + it('refuses a callback that is not callable', () => { + installPdfjsDomGlobals(); + const map = new Map(); + expect(() => + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the guard exists for callers without types + map.getOrInsertComputed( + 'a', + undefined as unknown as (k: string) => number, + ), + ).toThrow(TypeError); + }); +}); diff --git a/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.ts b/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.ts index cfe9bcf11a..88af27e2fe 100644 --- a/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.ts +++ b/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.ts @@ -1,8 +1,8 @@ 'use node'; /** - * Pure-JS DOM-global polyfills for running pdfjs-dist in the Convex node - * action runtime. + * Pure-JS DOM-global polyfills, plus the ES2025 shims pdfjs expects, for + * running pdfjs-dist on the backend's Node worker. * * pdfjs 5.x's Node setup polyfills `DOMMatrix` / `ImageData` / `Path2D` by * `require("@napi-rs/canvas")` — a NATIVE module. Convex extracts a bundled @@ -324,6 +324,37 @@ function ensureEs2025Shims(): void { }); } + // pdfjs 5.x calls this on plain Maps in both the main and worker builds + // (`_intentStates`, `methodPromises`, `objs`, …). Node 22 lacks it, and the + // failure is quiet: the operator-list read throws once per page, and that is + // the path that finds embedded images and decides "scanned page → OCR". Text + // extraction still runs, so a digital PDF looks fine while a scanned one + // indexes thin or empty with only a warning to say so (#3018). + // + // Spec shape: return the existing value when the key is present; otherwise + // compute, insert, and return. Presence, not truthiness — a callback that + // returns `undefined` still counts, so a second call does not recompute. + // `set` runs after the callback, as the proposal does: the callback may have + // inserted the key itself, and the computed value wins. + if (typeof Map.prototype.getOrInsertComputed !== 'function') { + // oxlint-disable-next-line no-extend-native -- deliberate spec-shaped polyfill of an ES2025 method Node 22 lacks; guarded so a real runtime implementation wins + Object.defineProperty(Map.prototype, 'getOrInsertComputed', { + value: function (this: Map, key: K, callbackfn: (k: K) => V) { + if (typeof callbackfn !== 'function') { + throw new TypeError( + 'Map.prototype.getOrInsertComputed: callback is not a function', + ); + } + if (this.has(key)) return this.get(key); + const value = callbackfn(key); + this.set(key, value); + return value; + }, + writable: true, + configurable: true, + }); + } + // Install when missing, or replace a process-global poison shim that // ignores `alphabet` (regression: always-`base64` loop). Native Node ≥24 // / Bun implementations honor options and win the probe below.