/** * 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 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 { 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 } }