mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-08-24 07:36:32 +00:00
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:
@@ -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';
|
||||
|
||||
@@ -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 {};
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user