wasm-rquickjs 0.2.2

Tool for wrapping JavaScript modules as WebAssembly components using the QuickJS engine
Documentation
/*! fetch-blob. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */

// 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536

/**
 * @param {(Blob | Uint8Array)[]} parts
 * @param {boolean} clone
 * @returns {AsyncIterableIterator<Uint8Array>}
 */
async function * toIterator (parts, clone) {
    for (const part of parts) {
        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
            }
        } else {
            // @ts-ignore TS Think blob.stream() returns a node:stream
            yield * part.stream()
        }
    }
}

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}`)
            }

            const size = ArrayBuffer.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 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<ArrayBuffer>}
     */
    async arrayBuffer () {
        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: `${type}` })
        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

const _File = class File extends Blob {
    #lastModified = 0
    #name = ''

    /**
     * @param {*[]} fileBits
     * @param {string} fileName
     * @param {{lastModified?: number, type?: string}} options
     */// @ts-ignore
    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)

        if (options === null) options = {}

        // Simulate WebIDL type casting for NaN value in lastModified option.
        const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified)
        if (!Number.isNaN(lastModified)) {
            this.#lastModified = lastModified
        }

        this.#name = String(fileName)
    }

    get name () {
        return this.#name
    }

    get lastModified () {
        return this.#lastModified
    }

    get [Symbol.toStringTag] () {
        return 'File'
    }

    static [Symbol.hasInstance] (object) {
        return !!object && object instanceof Blob &&
            /^(File)$/.test(object[Symbol.toStringTag])
    }
}

/** @type {typeof globalThis.File} */// @ts-ignore
export const File = _File