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