import { Injectable } from '@angular/core'; /** * Offloads keysToTreeControl and key sorting to a Web Worker. * Falls back to main-thread execution if Workers are unavailable. */ @Injectable({ providedIn: 'root' }) export class TreeBuilderService { private worker: Worker | null = null; private nextRequestId = 0; private pendingResolves = new Map void>(); constructor() { this.initWorker(); } private initWorker(): void { try { const blob = new Blob([ `(${workerFn.toString()})()` ], { type: 'application/javascript' }); this.worker = new Worker(URL.createObjectURL(blob)); this.worker.onmessage = (e: MessageEvent) => { const { _requestId, ...result } = e.data; const resolve = this.pendingResolves.get(_requestId); if (resolve) { this.pendingResolves.delete(_requestId); resolve(result); } }; this.worker.onerror = () => { this.worker = null; }; } catch { this.worker = null; } } /** * Build tree from keys — runs in Web Worker. */ keysToTreeControl(options: { keys: string[]; divider: string; keysInfo: any; savedExpandedNodes?: any[]; }): Promise<{ nodes: any[]; expandedNodes: any[] }> { if (this.worker) { const id = ++this.nextRequestId; return new Promise((resolve) => { this.pendingResolves.set(id, resolve); this.worker!.postMessage({ _requestId: id, action: 'buildTree', keys: options.keys, divider: options.divider, keysInfo: options.keysInfo, savedExpandedNodes: options.savedExpandedNodes ?? [], }); }); } return Promise.resolve(buildTreeSync(options)); } /** * Sort keys with natural compare — runs in Web Worker. */ sortKeys(keys: string[]): Promise { if (this.worker) { const id = ++this.nextRequestId; return new Promise((resolve) => { this.pendingResolves.set(id, (result: any) => resolve(result.keys)); this.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 }; }