/*! fetch-blob. MIT License. Jimmy Wärting */ // TODO (jimmywarting): in the feature use conditional loading with top level await (requires 14.x) // Node has recently added whatwg stream into core import './streams.cjs' // 64 KiB (same size chrome slice theirs blob into Uint8array's) const POOL_SIZE = 65536 /** @param {(Blob | Uint8Array)[]} parts */ async function * toIterator (parts, clone = true) { for (const part of parts) { if ('stream' in part) { yield * (/** @type {AsyncIterableIterator} */ (part.stream())) } else if (ArrayBuffer.isView(part)) { if (clone) { 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 } /* c8 ignore next 10 */ } else { // For blobs that have arrayBuffer but no stream method (nodes buffer.Blob) let position = 0, b = (/** @type {Blob} */ (part)) while (position !== b.size) { const chunk = b.slice(position, Math.min(b.size, position + POOL_SIZE)) const buffer = await chunk.arrayBuffer() position += buffer.byteLength yield new Uint8Array(buffer) } } } } const _Blob = 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 (ArrayBuffer.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}`) } this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.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 toIterator(this.#parts, false)) { 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 () { // Easier way... Just a unnecessary overhead // const view = new Uint8Array(this.size); // await this.stream().getReader({mode: 'byob'}).read(view); // return view.buffer; const data = new Uint8Array(this.size) let offset = 0 for await (const chunk of toIterator(this.#parts, false)) { data.set(chunk, offset) offset += chunk.length } return data.buffer } stream () { const it = toIterator(this.#parts, true) return new globalThis.ReadableStream({ // @ts-ignore 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 = 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 { let chunk if (ArrayBuffer.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: String(type).toLowerCase() }) blob.#size = span blob.#parts = blobParts return blob } get [Symbol.toStringTag] () { return 'Blob' } static [Symbol.hasInstance] (object) { return ( object && typeof object === 'object' && typeof object.constructor === 'function' && ( typeof object.stream === 'function' || typeof object.arrayBuffer === 'function' ) && /^(Blob|File)$/.test(object[Symbol.toStringTag]) ) } } Object.defineProperties(_Blob.prototype, { size: { enumerable: true }, type: { enumerable: true }, slice: { enumerable: true } }) /** @type {typeof globalThis.Blob} */ export const Blob = _Blob export default Blob