Skip to content

Commit

Permalink
file: embed polyfill for blob and file
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmywarting committed Nov 13, 2023
1 parent c7c5aa9 commit 2816d7d
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 13 deletions.
11 changes: 0 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
238 changes: 237 additions & 1 deletion src/js/polyfills/blob.js
Original file line number Diff line number Diff line change
@@ -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<Uint8Array>}
*/
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<string>}
*/
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<ArrayBuffer>}
*/
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,
Expand Down
52 changes: 52 additions & 0 deletions src/js/polyfills/file.js
Original file line number Diff line number Diff line change
@@ -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
});
1 change: 1 addition & 0 deletions src/js/polyfills/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 2816d7d

Please sign in to comment.