RSS Git Download  Clone
Raw Blame History 10kB 276 lines
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<number, (result: any) => 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<string[]> {
        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 };
}