const {Readable} = require('stream'); /** * @type {WeakMap} */ const wm = new WeakMap(); async function * read(parts) { for (const part of parts) { if ('stream' in part) { yield * part.stream(); } else { yield part; } } } class Blob { /** * 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 {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts * @param {{ type?: string }} [options] */ constructor(blobParts = [], options = {}) { let size = 0; const parts = blobParts.map(element => { let buffer; if (element instanceof Buffer) { buffer = element; } else if (ArrayBuffer.isView(element)) { buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); } else if (element instanceof ArrayBuffer) { buffer = Buffer.from(element); } else if (element instanceof Blob) { buffer = element; } else { buffer = Buffer.from(typeof element === 'string' ? element : String(element)); } // eslint-disable-next-line unicorn/explicit-length-check size += buffer.length || buffer.size || 0; return buffer; }); const type = options.type === undefined ? '' : String(options.type).toLowerCase(); wm.set(this, { type: /[^\u0020-\u007E]/.test(type) ? '' : type, size, parts }); } /** * The Blob interface's size property returns the * size of the Blob in bytes. */ get size() { return wm.get(this).size; } /** * The type property of a Blob object returns the MIME type of the file. */ get type() { return wm.get(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() { return Buffer.from(await this.arrayBuffer()).toString(); } /** * 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; } /** * The Blob interface's stream() method is difference from native * and uses node streams instead of whatwg streams. * * @returns {Readable} Node readable stream */ stream() { return Readable.from(read(wm.get(this).parts)); } /** * 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 = wm.get(this).parts.values(); const blobParts = []; let added = 0; for (const part of parts) { const size = ArrayBuffer.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 { const chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); blobParts.push(chunk); added += ArrayBuffer.isView(chunk) ? chunk.byteLength : chunk.size; relativeStart = 0; // All next sequental parts should start at 0 // don't add the overflow to new blobParts if (added >= span) { break; } } } const blob = new Blob([], {type: String(type).toLowerCase()}); Object.assign(wm.get(blob), {size: span, parts: blobParts}); return blob; } get [Symbol.toStringTag]() { return 'Blob'; } static [Symbol.hasInstance](object) { return ( object && typeof object === 'object' && typeof object.stream === 'function' && object.stream.length === 0 && typeof object.constructor === 'function' && /^(Blob|File)$/.test(object[Symbol.toStringTag]) ); } } Object.defineProperties(Blob.prototype, { size: {enumerable: true}, type: {enumerable: true}, slice: {enumerable: true} }); module.exports = Blob;