wasm-rquickjs 0.3.5

Tool for wrapping JavaScript modules as WebAssembly components using the QuickJS engine
Documentation
import * as encodingNative from '__wasm_rquickjs_builtin/encoding_native'
import * as streams from '__wasm_rquickjs_builtin/streams';
import {
    ERR_ENCODING_INVALID_ENCODED_DATA,
    ERR_ENCODING_NOT_SUPPORTED,
    ERR_INVALID_ARG_TYPE,
    ERR_INVALID_THIS,
    ERR_NO_ICU,
} from '__wasm_rquickjs_builtin/internal/errors';

const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
const textDecoderState = new WeakMap();
const textDecoderStreamState = new WeakSet();
const textEncoderStreamState = new WeakSet();

function validateOptions(options) {
    if (options !== undefined && options !== null && typeof options !== 'object') {
        throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
    }
}

function getTextDecoderState(thisArg) {
    const state = textDecoderState.get(thisArg);
    if (state === undefined) {
        throw new ERR_INVALID_THIS('TextDecoder');
    }
    return state;
}

function normalizeLabel(label) {
    const safeLabel = label === undefined ? 'utf-8' : `${label}`;
    const canonical = encodingNative.canonical_encoding(safeLabel);
    if (canonical === undefined || canonical === null) {
        throw new ERR_ENCODING_NOT_SUPPORTED(safeLabel);
    }
    return canonical;
}

function copyBytes(bytes) {
    const copy = new Uint8Array(bytes.byteLength);
    copy.set(bytes);
    return copy;
}

function concatBytes(first, second) {
    if (first.length === 0) return copyBytes(second);
    if (second.length === 0) return copyBytes(first);
    const result = new Uint8Array(first.length + second.length);
    result.set(first, 0);
    result.set(second, first.length);
    return result;
}

function trailingUtf8IncompleteLength(bytes) {
    const length = bytes.length;
    if (length === 0) return 0;

    let start = length - 1;
    while (start >= 0 && (bytes[start] & 0xc0) === 0x80 && length - start <= 4) {
        start--;
    }
    if (start < 0) return 0;

    const lead = bytes[start];
    let needed = 0;
    if (lead >= 0xc2 && lead <= 0xdf) needed = 2;
    else if (lead >= 0xe0 && lead <= 0xef) needed = 3;
    else if (lead >= 0xf0 && lead <= 0xf4) needed = 4;
    else return 0;

    const available = length - start;
    if (available >= 2) {
        const second = bytes[start + 1];
        if (lead === 0xe0 && second < 0xa0) return 0;
        if (lead === 0xed && second > 0x9f) return 0;
        if (lead === 0xf0 && second < 0x90) return 0;
        if (lead === 0xf4 && second > 0x8f) return 0;
    }
    return available < needed ? available : 0;
}

function readUtf16CodeUnit(bytes, offset, littleEndian) {
    return littleEndian
        ? bytes[offset] | (bytes[offset + 1] << 8)
        : (bytes[offset] << 8) | bytes[offset + 1];
}

function trailingUtf16IncompleteLength(bytes, littleEndian) {
    let pending = bytes.length % 2;
    const completeLength = bytes.length - pending;
    if (completeLength >= 2) {
        const lastUnit = readUtf16CodeUnit(bytes, completeLength - 2, littleEndian);
        if (lastUnit >= 0xd800 && lastUnit <= 0xdbff) {
            pending += 2;
        }
    }
    return pending;
}

function trailingIncompleteLength(bytes, encoding) {
    if (encoding === 'utf-8') return trailingUtf8IncompleteLength(bytes);
    if (encoding === 'utf-16le') return trailingUtf16IncompleteLength(bytes, true);
    if (encoding === 'utf-16be') return trailingUtf16IncompleteLength(bytes, false);
    return 0;
}

function toDecodeBytes(input) {
    if (input === undefined) {
        return new Uint8Array(0);
    }
    if (input instanceof ArrayBuffer) {
        try {
            return new Uint8Array(input);
        } catch (_) {
            return new Uint8Array(0);
        }
    }
    if (typeof SharedArrayBuffer !== 'undefined' && input instanceof SharedArrayBuffer) {
        return new Uint8Array(input);
    }
    if (ArrayBuffer.isView(input)) {
        try {
            if (input.buffer instanceof ArrayBuffer ||
                (typeof SharedArrayBuffer !== 'undefined' && input.buffer instanceof SharedArrayBuffer)) {
                return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
            }
        } catch (_) {
            return new Uint8Array(0);
        }
    }
    throw new ERR_INVALID_ARG_TYPE('input', 'an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView', input);
}

function decodeNative(bytes, state, stream) {
    const [result, error] = encodingNative.decode(bytes, state.encoding, stream, state.fatal, state.ignoreBOMForNextDecode);
    if (error !== undefined) {
        throw new ERR_ENCODING_INVALID_ENCODED_DATA(state.encoding);
    }
    return result;
}

export class TextDecoder {
    constructor(label, options) {
        validateOptions(options);
        const encoding = normalizeLabel(label);
        const fatal = !!options?.fatal;
        if (fatal) {
            throw new ERR_NO_ICU('fatal');
        }

        textDecoderState.set(this, {
            encoding,
            fatal,
            ignoreBOM: !!options?.ignoreBOM,
            ignoreBOMForNextDecode: !!options?.ignoreBOM,
            pending: new Uint8Array(0),
            streaming: false,
        });
    }

    get encoding() {
        return getTextDecoderState(this).encoding;
    }

    get fatal() {
        return getTextDecoderState(this).fatal;
    }

    get ignoreBOM() {
        return getTextDecoderState(this).ignoreBOM;
    }

    decode(buffer, options) {
        const state = getTextDecoderState(this);
        validateOptions(options);

        let bytes = toDecodeBytes(buffer);
        const stream = !!options?.stream;
        if (state.pending.length !== 0) {
            bytes = concatBytes(state.pending, bytes);
            state.pending = new Uint8Array(0);
        }

        if (stream) {
            const pendingLength = trailingIncompleteLength(bytes, state.encoding);
            if (pendingLength !== 0) {
                state.pending = bytes.slice(bytes.length - pendingLength);
                bytes = bytes.slice(0, bytes.length - pendingLength);
            }
            const result = decodeNative(bytes, state, true);
            if (bytes.length !== 0) {
                state.ignoreBOMForNextDecode = true;
            }
            state.streaming = true;
            return result;
        }

        const result = decodeNative(bytes, state, false);
        state.pending = new Uint8Array(0);
        state.ignoreBOMForNextDecode = state.ignoreBOM;
        state.streaming = false;
        return result;
    }

    [customInspectSymbol](depth, options) {
        const state = getTextDecoderState(this);
        if (depth < 0) {
            return '[TextDecoder]';
        }
        if (options?.showHidden) {
            return `TextDecoder { encoding: '${state.encoding}', fatal: ${state.fatal}, ignoreBOM: ${state.ignoreBOM} }`;
        }
        return `TextDecoder { encoding: '${state.encoding}', fatal: ${state.fatal}, ignoreBOM: ${state.ignoreBOM} }`;
    }
}

Object.defineProperties(TextDecoder.prototype, {
    [Symbol.toStringTag]: {
        value: 'TextDecoder',
        writable: false,
        enumerable: false,
        configurable: true,
    },
});

export class TextEncoder {
    constructor() {
    }

    get encoding() {
        return 'utf-8';
    }

    encode(input = '') {
        return encodingNative.encode(`${input}`);
    }

    encodeInto(string, uint8Array) {
        if (typeof string !== 'string') {
            throw new TypeError('The "src" argument must be of type string. Received type ' + typeof string);
        }
        return encodingNative.encode_into(string, uint8Array);
    }
}

export class TextDecoderStream extends streams.TransformStream {
    constructor(label, options) {
        validateOptions(options);
        const encoding = normalizeLabel(label);
        const fatal = !!options?.fatal;
        if (fatal) {
            throw new ERR_NO_ICU('fatal');
        }

        let decoder;
        super({
            start() {
                decoder = new TextDecoder(encoding, options);
            },
            transform(chunk, ctl) {
                const decoded = decoder.decode(chunk, { stream: true });
                if (decoded !== '') {
                    ctl.enqueue(decoded);
                }
            },
            flush(ctl) {
                const decoded = decoder.decode();
                if (decoded !== '') {
                    ctl.enqueue(decoded);
                }
                decoder = null;
            },
        });

        this._label = encoding;
        this._fatal = fatal;
        this._ignoreBOM = !!options?.ignoreBOM;
        textDecoderStreamState.add(this);
    }

    get encoding() {
        return this._label;
    }

    get fatal() {
        return this._fatal;
    }

    get ignoreBOM() {
        return this._ignoreBOM;
    }

    [customInspectSymbol]() {
        if (!textDecoderStreamState.has(this)) {
            throw new ERR_INVALID_THIS('TextDecoderStream');
        }
        return `TextDecoderStream {\n  encoding: '${this.encoding}',\n  fatal: ${this.fatal},\n  ignoreBOM: ${this.ignoreBOM},\n  readable: ReadableStream { locked: ${this.readable.locked}, state: 'readable', supportsBYOB: false },\n  writable: WritableStream { locked: ${this.writable.locked}, state: 'writable' }\n}`;
    }
}

export class TextEncoderStream extends streams.TransformStream {
    constructor() {
        let encoder;
        super({
            start() {
                encoder = new TextEncoder();
            },
            transform(chunk, ctl) {
                ctl.enqueue(encoder.encode(chunk));
            },
            flush() {
                encoder = null;
            },
        });
        textEncoderStreamState.add(this);
    }

    get encoding() {
        return 'utf-8';
    }

    [customInspectSymbol]() {
        if (!textEncoderStreamState.has(this)) {
            throw new ERR_INVALID_THIS('TextEncoderStream');
        }
        return `TextEncoderStream {\n  encoding: '${this.encoding}',\n  readable: ReadableStream { locked: ${this.readable.locked}, state: 'readable', supportsBYOB: false },\n  writable: WritableStream { locked: ${this.writable.locked}, state: 'writable' }\n}`;
    }
}