mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-13 17:54:19 +01: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:
3 files changed
+169
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
|
import './utils/map-upsert-polyfill.js';
|
||||||
import { categories } from './config/tools.js';
|
import { categories } from './config/tools.js';
|
||||||
import { dom, switchView, hideAlert } from './ui.js';
|
import { dom, switchView, hideAlert } from './ui.js';
|
||||||
import { ShortcutsManager } from './logic/shortcuts.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