Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>();
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<string, number>([['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<string, undefined>();
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<string, number>();
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);
});
});
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 <K, V>(this: Map<K, V>, 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.
Expand Down
Loading