Skip to content
Closed
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,112 @@ 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('lets the computed value win when the callback inserted the key itself', () => {
// The proposal re-checks after the callback and overwrites, so a callback
// with a side effect cannot leave the map disagreeing with the return.
installPdfjsDomGlobals();
const map = new Map<string, number>();
const value = map.getOrInsertComputed('a', () => {
map.set('a', 99);
return 1;
});
expect(value).toBe(1);
expect(map.get('a')).toBe(1);
});

it('rejects a callback that is not callable, even when the key exists', () => {
// Spec order: the callable check comes BEFORE the lookup. Without it, a
// present key would return happily and a bad callback would go unnoticed
// until some later call happened to miss.
installPdfjsDomGlobals();
const present = new Map<string, number>([['a', 1]]);
expect(() =>
(
present as unknown as {
getOrInsertComputed: (k: string, f: unknown) => void;
}
).getOrInsertComputed('a', 'not a function'),
).toThrow(TypeError);

const absent = new Map<string, number>();
expect(() =>
(
absent as unknown as {
getOrInsertComputed: (k: string, f: unknown) => void;
}
).getOrInsertComputed('a', 'not a function'),
).toThrow(TypeError);
});

it('leaves a real runtime implementation alone', () => {
// Guarded like every other shim here, so Node ≥24 wins.
const marker = function (this: Map<unknown, unknown>) {
return 'native';
};
// oxlint-disable-next-line no-extend-native -- test stands in for a native implementation
Object.defineProperty(Map.prototype, 'getOrInsertComputed', {
value: marker,
writable: true,
configurable: true,
});
try {
installPdfjsDomGlobals();
expect(Map.prototype.getOrInsertComputed).toBe(marker);
} finally {
// The guard will not replace a function, so the stand-in has to be
// removed here or every later suite in this worker inherits it.
delete (Map.prototype as { getOrInsertComputed?: unknown })
.getOrInsertComputed;
installPdfjsDomGlobals();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,36 @@ function ensureEs2025Shims(): void {
configurable: true,
});
}
// 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 per page, which 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 if the key is present; otherwise
// compute, insert, and return. `set` after the callback is what the proposal
// does too — the callback may itself have inserted the key, 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,
});
}

if (typeof Uint8Array.prototype.toHex !== '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(Uint8Array.prototype, 'toHex', {
Expand Down
Loading