Merge pull request #795 from mvanhorn/fix/662-map-upsert-polyfill

fix: polyfill Map.prototype.getOrInsertComputed to stop PDF.js render crash
This commit is contained in:
Alam
2026-08-22 15:58:31 +05:30
committed by GitHub
3 changed files with 169 additions and 0 deletions
+1
View File
@@ -1,3 +1,4 @@
import './utils/map-upsert-polyfill.js';
import { categories } from './config/tools.js';
import { dom, switchView, hideAlert } from './ui.js';
import { ShortcutsManager } from './logic/shortcuts.js';
+47
View File
@@ -0,0 +1,47 @@
type MapPrototypeWithUpsert = typeof Map.prototype & {
getOrInsert?: (key: unknown, value: unknown) => unknown;
getOrInsertComputed?: (
key: unknown,
callback: (key: unknown) => unknown
) => unknown;
};
const mapPrototype = Map.prototype as MapPrototypeWithUpsert;
const mapHas = Map.prototype.has;
const mapGet = Map.prototype.get;
const mapSet = Map.prototype.set;
if (typeof mapPrototype.getOrInsert !== 'function') {
Object.defineProperty(mapPrototype, 'getOrInsert', {
configurable: true,
writable: true,
value: function getOrInsert(key: unknown, value: unknown) {
if (mapHas.call(this, key)) return mapGet.call(this, key);
mapSet.call(this, key, value);
return value;
},
});
}
if (typeof mapPrototype.getOrInsertComputed !== 'function') {
Object.defineProperty(mapPrototype, 'getOrInsertComputed', {
configurable: true,
writable: true,
value: function getOrInsertComputed(
key: unknown,
callback: (key: unknown) => unknown
) {
const hasKey = mapHas.call(this, key);
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
if (hasKey) return mapGet.call(this, key);
const value = callback(key);
mapSet.call(this, key, value);
return value;
},
});
}
export {};
+121
View File
@@ -0,0 +1,121 @@
import { readFileSync } from 'node:fs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
type MapWithUpsert<K, V> = Map<K, V> & {
getOrInsert(key: K, value: V): V;
getOrInsertComputed(key: K, callback: (key: K) => V): V;
};
const getOrInsertDescriptor = Object.getOwnPropertyDescriptor(
Map.prototype,
'getOrInsert'
);
const getOrInsertComputedDescriptor = Object.getOwnPropertyDescriptor(
Map.prototype,
'getOrInsertComputed'
);
const restoreMethod = (
name: 'getOrInsert' | 'getOrInsertComputed',
descriptor: PropertyDescriptor | undefined
) => {
if (descriptor) {
Object.defineProperty(Map.prototype, name, descriptor);
} else {
Reflect.deleteProperty(Map.prototype, name);
}
};
const importPolyfill = async () => {
await import('../js/utils/map-upsert-polyfill');
};
describe('Map upsert polyfill', () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
restoreMethod('getOrInsert', getOrInsertDescriptor);
restoreMethod('getOrInsertComputed', getOrInsertComputedDescriptor);
vi.resetModules();
});
it('preserves an existing native implementation', async () => {
const nativeImplementation = vi.fn();
Object.defineProperty(Map.prototype, 'getOrInsertComputed', {
configurable: true,
writable: true,
value: nativeImplementation,
});
await importPolyfill();
expect(
Object.getOwnPropertyDescriptor(Map.prototype, 'getOrInsertComputed')
?.value
).toBe(nativeImplementation);
});
it('installs getOrInsertComputed when it is missing', async () => {
Reflect.deleteProperty(Map.prototype, 'getOrInsertComputed');
await importPolyfill();
const descriptor = Object.getOwnPropertyDescriptor(
Map.prototype,
'getOrInsertComputed'
);
expect(typeof descriptor?.value).toBe('function');
expect(descriptor?.enumerable).toBe(false);
});
it('returns an existing value without invoking the callback', async () => {
Reflect.deleteProperty(Map.prototype, 'getOrInsertComputed');
await importPolyfill();
const map = new Map([['key', 'cached']]) as MapWithUpsert<string, string>;
const callback = vi.fn(() => 'computed');
expect(map.getOrInsertComputed('key', callback)).toBe('cached');
expect(callback).not.toHaveBeenCalled();
});
it('computes, stores, and returns a missing value', async () => {
Reflect.deleteProperty(Map.prototype, 'getOrInsertComputed');
await importPolyfill();
const map = new Map<string, string>() as MapWithUpsert<string, string>;
const callback = vi.fn((key: string) => `${key}-value`);
expect(map.getOrInsertComputed('missing', callback)).toBe('missing-value');
expect(callback).toHaveBeenCalledOnce();
expect(callback).toHaveBeenCalledWith('missing');
expect(map.get('missing')).toBe('missing-value');
});
it('installs getOrInsert with matching get-or-insert behavior', async () => {
Reflect.deleteProperty(Map.prototype, 'getOrInsert');
await importPolyfill();
const map = new Map([['existing', 'cached']]) as MapWithUpsert<
string,
string
>;
expect(map.getOrInsert('existing', 'replacement')).toBe('cached');
expect(map.getOrInsert('missing', 'inserted')).toBe('inserted');
expect(map.get('missing')).toBe('inserted');
});
it('loads before PDF.js so render can use the methods', () => {
const mainSource = readFileSync(
new URL('../js/main.ts', import.meta.url),
'utf8'
);
const polyfillImport = "import './utils/map-upsert-polyfill.js';";
const pdfjsImport = "import * as pdfjsLib from 'pdfjs-dist';";
expect(mainSource.startsWith(polyfillImport)).toBe(true);
expect(mainSource.indexOf(polyfillImport)).toBeLessThan(
mainSource.indexOf(pdfjsImport)
);
});
});