RSS Git Download  Clone
Raw Blame History 9kB 226 lines
/**
 * Tree builder service — exact port of Angular TreeBuilderService.
 * Offloads key sorting and tree building to a Web Worker.
 * Falls back to main-thread execution if Workers are unavailable.
 */

let worker: Worker | null = null
let nextRequestId = 0
const pendingResolves = new Map<number, (result: any) => void>()

function initWorker() {
    try {
        const blob = new Blob([
            `(${workerFn.toString()})()`
        ], { type: 'application/javascript' })
        worker = new Worker(URL.createObjectURL(blob))
        worker.onmessage = (e: MessageEvent) => {
            const { _requestId, ...result } = e.data
            const resolve = pendingResolves.get(_requestId)
            if (resolve) {
                pendingResolves.delete(_requestId)
                resolve(result)
            }
        }
        worker.onerror = () => { worker = null }
    } catch {
        worker = null
    }
}

// Initialize worker on module load
initWorker()

export function keysToTreeControl(options: {
    keys: string[]
    divider: string
    keysInfo: any
    savedExpandedNodes?: any[]
}): Promise<{ nodes: any[]; expandedNodes: any[] }> {
    if (worker) {
        const id = ++nextRequestId
        return new Promise((resolve) => {
            pendingResolves.set(id, resolve)
            worker!.postMessage({
                _requestId: id,
                action: 'buildTree',
                keys: options.keys,
                divider: options.divider,
                keysInfo: options.keysInfo,
                savedExpandedNodes: options.savedExpandedNodes ?? [],
            })
        })
    }
    return Promise.resolve(buildTreeSync(options))
}

export function sortKeys(keys: string[]): Promise<string[]> {
    if (worker) {
        const id = ++nextRequestId
        return new Promise((resolve) => {
            pendingResolves.set(id, (result: any) => resolve(result.keys))
            worker!.postMessage({ _requestId: id, action: 'sortKeys', keys })
        })
    }
    return Promise.resolve(keys.sort(naturalCompare()))
}

// Worker function — serialized into a Blob URL
function workerFn() {
    const naturalCompare = () => {
        return (a: string, b: string) => {
            const regex = /(\d+)|(\D+)/g
            const ax: any[] = [], bx: any[] = []
            a.replace(regex, (_: any, $1: any, $2: any) => { ax.push([$1 || Infinity, $2 || '']); return '' })
            b.replace(regex, (_: any, $1: any, $2: any) => { bx.push([$1 || Infinity, $2 || '']); return '' })
            while (ax.length && bx.length) {
                const an = ax.shift()!
                const bn = bx.shift()!
                const nn = (parseFloat(an[0]) - parseFloat(bn[0])) || an[1].localeCompare(bn[1])
                if (nn) return nn
            }
            return ax.length - bx.length
        }
    }

    const buildTree = (keys: string[], divider: string, keysInfo: any, savedExpandedNodes: any[]) => {
        const mainNodes: any[] = []
        const newExpandedNodes: any[] = []
        const saved = savedExpandedNodes || []

        const recursiveNodes = (splitKey: string[], level = 0, nodes: any[] = mainNodes) => {
            let foundNode: any = false
            if (level + 1 < splitKey.length) {
                for (let i = 0; i < nodes.length; i++) {
                    if (nodes[i].label === splitKey[level] && nodes[i].type === 'folder') {
                        foundNode = nodes[i]
                        break
                    }
                }
            }
            if (!foundNode) {
                const node: any = {
                    label: splitKey[level],
                    key: splitKey.slice(0, level + 1).join(divider),
                    children: [],
                    childCount: 0,
                    type: level + 1 === splitKey.length ? 'element' : 'folder',
                }
                if (node.type === 'element' && keysInfo) node.keysInfo = keysInfo[node.key]
                nodes.push(node)
                foundNode = node
                for (let j = 0; j < saved.length; j++) {
                    if (saved[j].key === foundNode.key) newExpandedNodes.push(foundNode)
                }
            }
            if (level + 1 < splitKey.length) recursiveNodes(splitKey, level + 1, foundNode.children)
        }

        for (let i = 0; i < keys.length; i++) {
            recursiveNodes(divider === '' ? [keys[i]] : keys[i].split(divider))
        }

        const recursiveKeyCount = (node: any) => {
            node.childCount = 0
            for (let i = 0; i < node.children.length; i++) {
                if (node.children[i].type === 'element') {
                    const info = node.children[i].keysInfo
                    if (info && info.type !== 'string' && info.type !== 'json' && info.length != null) {
                        node.childCount += info.length
                    } else { node.childCount++ }
                }
            }
            for (let i = 0; i < node.children.length; i++) {
                recursiveKeyCount(node.children[i])
                if (node.children[i].type === 'folder') node.childCount += node.children[i].childCount
            }
        }

        for (let i = 0; i < mainNodes.length; i++) recursiveKeyCount(mainNodes[i])
        return { nodes: mainNodes, expandedNodes: newExpandedNodes }
    }

    ;(self as any).onmessage = function (e: MessageEvent) {
        const data = e.data
        const _requestId = data._requestId
        if (data.action === 'sortKeys') {
            const sorted = data.keys.sort(naturalCompare())
            ;(self as any).postMessage({ _requestId, keys: sorted })
        } else if (data.action === 'buildTree') {
            const result = buildTree(data.keys, data.divider, data.keysInfo, data.savedExpandedNodes)
            ;(self as any).postMessage({ _requestId, ...result })
        }
    }
}

// Main-thread fallbacks
function naturalCompare() {
    return (a: string, b: string) => {
        const regex = /(\d+)|(\D+)/g
        const ax: any[] = [], bx: any[] = []
        a.replace(regex, (_: any, $1: any, $2: any) => { ax.push([$1 || Infinity, $2 || '']); return '' })
        b.replace(regex, (_: any, $1: any, $2: any) => { bx.push([$1 || Infinity, $2 || '']); return '' })
        while (ax.length && bx.length) {
            const an = ax.shift()!
            const bn = bx.shift()!
            const nn = (parseFloat(an[0]) - parseFloat(bn[0])) || an[1].localeCompare(bn[1])
            if (nn) return nn
        }
        return ax.length - bx.length
    }
}

function buildTreeSync(options: {
    keys: string[]
    divider: string
    keysInfo: any
    savedExpandedNodes?: any[]
}): { nodes: any[]; expandedNodes: any[] } {
    const { keys, divider, keysInfo } = options
    const saved = options.savedExpandedNodes ?? []
    const mainNodes: any[] = []
    const newExpandedNodes: any[] = []

    const recursiveNodes = (splitKey: string[], level = 0, nodes: any[] = mainNodes) => {
        let foundNode: any = false
        if (level + 1 < splitKey.length) {
            for (const node of nodes) {
                if (node.label === splitKey[level] && node.type === 'folder') { foundNode = node; break }
            }
        }
        if (!foundNode) {
            const node: any = {
                label: splitKey[level],
                key: splitKey.slice(0, level + 1).join(divider),
                children: [], childCount: 0,
                type: level + 1 === splitKey.length ? 'element' : 'folder',
            }
            if (node.type === 'element' && keysInfo) node.keysInfo = keysInfo[node.key]
            nodes.push(node)
            foundNode = node
            for (const s of saved) { if (s.key === foundNode.key) newExpandedNodes.push(foundNode) }
        }
        if (level + 1 < splitKey.length) recursiveNodes(splitKey, level + 1, foundNode.children)
    }

    for (const key of keys) recursiveNodes(divider === '' ? [key] : key.split(divider))

    const recursiveKeyCount = (node: any) => {
        node.childCount = 0
        for (const child of node.children) {
            if (child.type === 'element') {
                const info = child.keysInfo
                if (info && info.type !== 'string' && info.type !== 'json' && info.length != null) node.childCount += info.length
                else node.childCount += 1
            }
        }
        for (const child of node.children) {
            recursiveKeyCount(child)
            if (child.type === 'folder') node.childCount += child.childCount
        }
    }

    for (const node of mainNodes) recursiveKeyCount(node)
    return { nodes: mainNodes, expandedNodes: newExpandedNodes }
}