All files / src state-utils.js

100% Statements 31/31
100% Branches 10/10
100% Functions 4/4
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65            191x   191x 191x 191x 595x 595x   191x                   181x 181x 181x 3307020x     181x 181x 181x 1x       180x 180x 180x                   45x 23x   22x 9x   13x 6x   11x 11x 30x   11x    
/**
 * Convert a TypedArray to a base64 string for JSON serialization.
 * @param {ArrayBufferView} typedArray
 * @returns {string} base64-encoded string
 */
export function typedArrayToBase64(typedArray) {
    const bytes = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
    // Build binary string in chunks to avoid excessive string concatenation
    const chunkSize = 8192;
    const parts = [];
    for (let i = 0; i < bytes.length; i += chunkSize) {
        const end = Math.min(i + chunkSize, bytes.length);
        parts.push(String.fromCharCode(...bytes.subarray(i, end)));
    }
    return btoa(parts.join(""));
}
 
/**
 * Convert a base64 string back to a TypedArray.
 * @param {string} base64 base64-encoded string
 * @param {function} TypedArrayConstructor constructor for the desired type (e.g., Uint8Array)
 * @returns {ArrayBufferView} the decoded typed array
 */
export function base64ToTypedArray(base64, TypedArrayConstructor) {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
        bytes[i] = binary.charCodeAt(i);
    }
    // Create a properly aligned typed array from the raw bytes
    const elementSize = TypedArrayConstructor.BYTES_PER_ELEMENT;
    const length = bytes.length / elementSize;
    if (!Number.isInteger(length)) {
        throw new Error(
            `Base64 data length (${bytes.length} bytes) is not a multiple of ${TypedArrayConstructor.name} element size (${elementSize})`,
        );
    }
    const result = new TypedArrayConstructor(length);
    new Uint8Array(result.buffer).set(bytes);
    return result;
}
 
/**
 * Deep copy a snapshot object, cloning any TypedArrays found within.
 * This ensures rewind buffer snapshots are fully isolated from live state.
 * @param {object} obj snapshot object to copy
 * @returns {object} a deep copy with all TypedArrays cloned
 */
export function deepCopySnapshot(obj) {
    if (obj === null || typeof obj !== "object") {
        return obj;
    }
    if (ArrayBuffer.isView(obj)) {
        return obj.slice();
    }
    if (Array.isArray(obj)) {
        return obj.map((item) => deepCopySnapshot(item));
    }
    const copy = {};
    for (const key of Object.keys(obj)) {
        copy[key] = deepCopySnapshot(obj[key]);
    }
    return copy;
}