From 2816d7d8f55056167cfe18b223da285ede29f3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Mon, 13 Nov 2023 01:30:35 +0100 Subject: [PATCH] file: embed polyfill for blob and file --- package-lock.json | 11 -- package.json | 1 - src/js/polyfills/blob.js | 238 +++++++++++++++++++++++++++++++++++++- src/js/polyfills/file.js | 52 +++++++++ src/js/polyfills/index.js | 1 + 5 files changed, 290 insertions(+), 13 deletions(-) create mode 100644 src/js/polyfills/file.js diff --git a/package-lock.json b/package-lock.json index 98a5a8fe..ad7ea417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "license": "MIT", "dependencies": { "abortcontroller-polyfill": "^1.7.5", - "blob-polyfill": "^7.0.20220408", "core-js": "^3.25.5", "esbuild": "^0.15.11", "eslint": "^8.24.0", @@ -308,11 +307,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/blob-polyfill": { - "version": "7.0.20220408", - "resolved": "https://registry.npmjs.org/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz", - "integrity": "sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ==" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3334,11 +3328,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "blob-polyfill": { - "version": "7.0.20220408", - "resolved": "https://registry.npmjs.org/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz", - "integrity": "sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ==" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index 52587226..37e71c19 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "license": "MIT", "dependencies": { "abortcontroller-polyfill": "^1.7.5", - "blob-polyfill": "^7.0.20220408", "core-js": "^3.25.5", "esbuild": "^0.15.11", "eslint": "^8.24.0", diff --git a/src/js/polyfills/blob.js b/src/js/polyfills/blob.js index 468cf8cd..3c7841f4 100644 --- a/src/js/polyfills/blob.js +++ b/src/js/polyfills/blob.js @@ -1,4 +1,240 @@ -import { Blob } from 'blob-polyfill'; +// 64 KiB (same size chrome slice theirs blob into Uint8array's) +const POOL_SIZE = 65536; +const { isView } = ArrayBuffer; + +/** + * @param {(Blob | Uint8Array)[]} parts + * @returns {AsyncIterableIterator} + */ +async function * toIterator (parts) { + for (const part of parts) { + if (isView(part)) { + let position = part.byteOffset; + const end = part.byteOffset + part.byteLength; + + while (position !== end) { + const size = Math.min(end - position, POOL_SIZE); + const chunk = part.buffer.slice(position, position + size); + + position += chunk.byteLength; + yield new Uint8Array(chunk); + } + } else { + yield * part.stream(); + } + } +} + +class Blob { + /** @type {Array.<(Blob|Uint8Array)>} */ + #parts = []; + #type = ''; + #size = 0; + #endings = 'transparent'; + + /** + * The Blob() constructor returns a new Blob object. The content + * of the blob consists of the concatenation of the values given + * in the parameter array. + * + * @param {*} blobParts + * @param {{ type?: string, endings?: string }} [options] + */ + constructor (blobParts = [], options = {}) { + if (typeof blobParts !== 'object' || blobParts === null) { + throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'); + } + + if (typeof blobParts[Symbol.iterator] !== 'function') { + throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.'); + } + + if (typeof options !== 'object' && typeof options !== 'function') { + throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.'); + } + + if (options === null) { + options = {}; + } + + + const encoder = new TextEncoder(); + + for (const element of blobParts) { + let part; + + if (isView(element)) { + part = new Uint8Array( + element.buffer.slice( + element.byteOffset, + element.byteOffset + element.byteLength + ) + ); + } else if (element instanceof ArrayBuffer) { + part = new Uint8Array(element.slice(0)); + } else if (element instanceof Blob) { + part = element; + } else { + part = encoder.encode(`${element}`); + } + + const size = isView(part) ? part.byteLength : part.size; + + // Avoid pushing empty parts into the array to better GC them + if (size) { + this.#size += size; + this.#parts.push(part); + } + } + + this.#endings = `${options.endings === undefined ? 'transparent' : options.endings}`; + const type = options.type === undefined ? '' : String(options.type); + + this.#type = /^[\x20-\x7E]*$/.test(type) ? type : ''; + } + + /** + * The Blob interface's size property returns the + * size of the Blob in bytes. + */ + get size () { + return this.#size; + } + + /** + * The type property of a Blob object returns the MIME type of the file. + */ + get type () { + return this.#type; + } + + /** + * The text() method in the Blob interface returns a Promise + * that resolves with a string containing the contents of + * the blob, interpreted as UTF-8. + * + * @return {Promise} + */ + async text () { + // More optimized than using this.arrayBuffer() + // that requires twice as much ram + const decoder = new TextDecoder(); + let str = ''; + + for await (const part of this.stream()) { + str += decoder.decode(part, { stream: true }); + } + + // Remaining + str += decoder.decode(); + + return str; + } + + /** + * The arrayBuffer() method in the Blob interface returns a + * Promise that resolves with the contents of the blob as + * binary data contained in an ArrayBuffer. + * + * @return {Promise} + */ + async arrayBuffer () { + const data = new Uint8Array(this.size); + let offset = 0; + + for await (const chunk of this.stream()) { + data.set(chunk, offset); + offset += chunk.length; + } + + return data.buffer; + } + + stream () { + const it = toIterator(this.#parts); + + return new ReadableStream({ + type: 'bytes', + async pull (ctrl) { + const chunk = await it.next(); + + chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value); + }, + + async cancel () { + await it.return(); + } + }); + } + + /** + * The Blob interface's slice() method creates and returns a + * new Blob object which contains data from a subset of the + * blob on which it's called. + * + * @param {number} [start] + * @param {number} [end] + * @param {string} [type] + */ + slice (start = 0, end = this.size, type = '') { + const { size } = this; + + let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); + let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); + + const span = Math.max(relativeEnd - relativeStart, 0); + const parts = this.#parts; + const blobParts = []; + let added = 0; + + for (const part of parts) { + // don't add the overflow to new blobParts + if (added >= span) { + break; + } + + const size = isView(part) ? part.byteLength : part.size; + + if (relativeStart && size <= relativeStart) { + // Skip the beginning and change the relative + // start & end position as we skip the unwanted parts + relativeStart -= size; + relativeEnd -= size; + } else { + let chunk; + + if (isView(part)) { + chunk = part.subarray(relativeStart, Math.min(size, relativeEnd)); + added += chunk.byteLength; + } else { + chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); + added += chunk.size; + } + + relativeEnd -= size; + blobParts.push(chunk); + relativeStart = 0; // All next sequential parts should start at 0 + } + } + + const blob = new Blob([], { type: `${type}` }); + + blob.#size = span; + blob.#parts = blobParts; + + return blob; + } + + get [Symbol.toStringTag] () { + return 'Blob'; + } +} + +Object.defineProperties(Blob.prototype, { + size: { enumerable: true }, + type: { enumerable: true }, + slice: { enumerable: true } +}); Object.defineProperty(window, 'Blob', { enumerable: true, diff --git a/src/js/polyfills/file.js b/src/js/polyfills/file.js new file mode 100644 index 00000000..6c707e61 --- /dev/null +++ b/src/js/polyfills/file.js @@ -0,0 +1,52 @@ +const { now } = Date; +const { isNaN } = Number; + +class File extends Blob { + #lastModified = 0; + #name = ''; + + /** + * @param {*[]} fileBits + * @param {string} fileName + * @param {{lastModified?: number, type?: string}} options + */ + constructor (fileBits, fileName, options = {}) { + if (arguments.length < 2) { + throw new TypeError( + `Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.` + ); + } + + super(fileBits, options); + + // Simulate WebIDL type casting for NaN value in lastModified option. + const lastModified = options.lastModified === undefined + ? now() + : Number(options.lastModified); + + if (!isNaN(lastModified)) { + this.#lastModified = lastModified; + } + + this.#name = String(fileName); + } + + get name () { + return this.#name; + } + + get lastModified () { + return this.#lastModified; + } + + get [Symbol.toStringTag] () { + return 'File'; + } +} + +Object.defineProperty(window, 'File', { + enumerable: true, + configurable: true, + writable: true, + value: File +}); diff --git a/src/js/polyfills/index.js b/src/js/polyfills/index.js index 9742929b..08f9d40e 100644 --- a/src/js/polyfills/index.js +++ b/src/js/polyfills/index.js @@ -13,6 +13,7 @@ import 'web-streams-polyfill/es2018'; import './text-encoding.js'; import './blob.js'; +import './file.js'; import './console.js'; import './crypto.js'; import './performance.js';