obj2u8arr.mjs

import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import isbol from './isbol.mjs'
import isarr1 from './isarr1.mjs'
import isab from './isab.mjs'
import iseobj from './iseobj.mjs'
import getBufferSize from './getBufferSize.mjs'
import obj2stru8arr from './obj2stru8arr.mjs'
import str2u8arr from './str2u8arr.mjs'
import bufWriteDbl from './bufWriteDbl.mjs'


//toU8arrView, 取得逐位元組之Uint8Array視圖
//注意不可用new Uint8Array(typedArray), 那是「逐元素」轉換, 每個元素會被截斷為uint8; 對Uint8Array兩者恰好相同, 對Uint16Array則會遺失高位元組(如Uint16Array([300])得[44]而非[44,1])
function toU8arrView(b) {
    if (isab(b)) {
        return new Uint8Array(b) //ArrayBuffer本身即位元組來源, 此處為位元組視圖非逐元素轉換
    }
    if (ArrayBuffer.isView(b)) {
        return new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
    }
    return new Uint8Array(0)
}


// function concatU8arr(a, b) { //處理大檔時Nodejs記憶體會不足
//     return Uint8Array.from([...a, ...b])
// }
//mergeU8arrs, 合併各分塊為單一Uint8Array
//不可逐次兩兩concat, 那會每次重新配置並複製已累積之全部, 總計為O(n^2)且峰值記憶體為結果之2倍(舊新陣列並存); 改為先算總長一次配置後依偏移量寫入, 為O(n)且峰值記憶體為1倍
function mergeU8arrs(bs) {

    //n, 總長度
    let n = 0
    each(bs, (b) => {
        n += getBufferSize(b)
    })

    //r
    let r = new Uint8Array(n)
    let i = 0
    each(bs, (b) => {
        let v = toU8arrView(b)
        r.set(v, i)
        i += v.length
    })

    return r
}


/**
 * 物件或陣列資料轉Uint8Array
 *
 * 因底層以JSON.stringify序列化,下列型別會靜默失真且不會拋錯,須由呼叫端自行避免:NaN與Infinity轉為null;值為undefined、函數或Symbol者該鍵會消失;Map、Set、RegExp轉為{};Date轉為ISO字串;稀疏陣列之空洞補為null;超出Number精度之大整數會失精。另BigInt與循環參照會使序列化失敗,回傳空Uint8Array
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/obj2u8arr.test.mjs Github}
 * @memberOf wsemi
 * @param {Object|Array} data 輸入物件或陣列資料,物件內可支援Uint8Array、Uint16Array、ArrayBuffer,三者皆以逐位元組方式打包,並由標記記錄原型別,故u8arr2obj可原型別原數值還原
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.returnWithStateAndMsg=false] 輸入是否回傳含狀態與訊息物件布林值,若為true則回傳{ state, msg }物件,state為'success'或'error',msg於success時為回傳結果、於error時為錯誤訊息字串,預設false
 * @returns {Uint8Array|Object} 回傳Uint8Array,輸入非有效物件或陣列、或序列化失敗(如含BigInt、循環參照)時回傳空Uint8Array;若opt.returnWithStateAndMsg為true則回傳{ state, msg }物件
 * @example
 *
 * let data = {
 *     a: [123, 45.67, 'test中文'],
 *     b: {
 *         c: new Uint8Array([66, 97, 115]),
 *     },
 * }
 * let u8a = obj2u8arr(data)
 * console.log(u8a)
 * // => Uint8Array [
 * //     64,  24,   0,   0,   0,  0,  0,   0,  91,  54,  54,  44,
 * //     51,  93, 123,  34,  97, 34, 58,  91,  49,  50,  51,  44,
 * //     52,  53,  46,  54,  55, 44, 34, 116, 101, 115, 116, 228,
 * //    184, 173, 230, 150, 135, 34, 93,  44,  34,  98,  34,  58,
 * //    123,  34,  99,  34,  58, 34, 91,  66, 108,  97, 122, 101,
 * //     70, 111, 114,  85, 105, 110, 116, 56,  65, 114, 114,  97,
 * //    121,  93,  58,  58,  48, 34, 125, 125,  66,  97, 115
 * // ]
 *
 * console.log(obj2u8arr({ id: 1n }, { returnWithStateAndMsg: true }))
 * // => {
 * //     state: 'error',
 * //     msg: 'obj2stru8arr: TypeError: Do not know how to serialize a BigInt'
 * // }
 *
 */
function obj2u8arr(data, opt = {}) {
    let bs = []
    let r = new Uint8Array()

    //returnWithStateAndMsg
    let returnWithStateAndMsg = get(opt, 'returnWithStateAndMsg', null)
    if (!isbol(returnWithStateAndMsg)) {
        returnWithStateAndMsg = false
    }

    //retError
    let retError = (msg) => {
        if (returnWithStateAndMsg) {
            return {
                state: 'error',
                msg,
            }
        }
        else {
            return new Uint8Array()
        }
    }

    //check, 陣列以isarr1判「是陣列且長度大於0」, 不可用isearr, 因isearr於長度為1時會額外檢查該元素是否有效, 使['']、[null]這類單元素陣列被整個拒絕, 而['','a']卻可通過, 造成同型輸入因長度而結果不同
    if (!isarr1(data) && !iseobj(data)) {
        return retError('invalid data, data is not an effective object or a non-empty array')
    }

    //addBin
    let pkLens = []
    let pkBins = []
    function addBin(b) {
        pkLens.push(getBufferSize(b))
        pkBins.push(b)
    }

    try {

        //obj2stru8arr, 內部一律取狀態, 逐一判識後才把ret交給下一步; 序列化失敗時不得續產出封包(否則會編出一個結構合法但內容為空之封包, 解碼端無從辨識), 錯誤訊息前置來源函數名以利分辨是哪一步出錯
        let rsb = obj2stru8arr(data, { returnWithStateAndMsg: true }) //序列化數據, 分別為無Uint8Array序列化字串(results), 以及各Uint8Array數據(binarys)
        if (rsb.state === 'error') {
            return retError(`obj2stru8arr: ${rsb.msg}`)
        }
        let sb = rsb.msg
        //console.log('sb', sb)

        //sb.results
        let bMain = str2u8arr(sb.results) //無Uint8Array序列化字串轉二進位數據(Uint8Array)
        addBin(bMain) //加入無Uint8Array序列化字串二進位數據(Uint8Array)

        //sb.binarys
        each(sb.binarys, (b) => {
            addBin(b) //加入各分塊二進位數據(Uint8Array)
        })

        //bPks
        let vPks = pkLens //各分塊長度資訊
        let bPks = str2u8arr(JSON.stringify(vPks)) //各分塊長度資訊序列化成字串, 再轉二進位數據(Uint8Array)

        //push head
        let ibHead = 8 //預設用開頭8 bytes來儲存分塊資訊之長度
        let bHead = new Uint8Array(ibHead) //宣告
        bufWriteDbl(getBufferSize(bPks), bHead) //寫入分塊資訊之長度
        bs.push(bHead) //推入開頭儲存分塊資訊之長度

        //push
        bs.push(bPks) //推入各分塊資訊長度陣列

        //push
        each(pkBins, (b) => {
            bs.push(b) //推入各分塊資訊
        })

        //flatten
        r = mergeU8arrs(bs) //合併各二進位數據

    }
    catch (err) {
        return retError(err.toString())
    }

    if (returnWithStateAndMsg) {
        return {
            state: 'success',
            msg: r,
        }
    }
    else {
        return r
    }
}


export default obj2u8arr