UNPKG

@zenithcore/runtime

Version:

Runtime codec implementation for ZenithKernel framework

1,409 lines (1,174 loc) 69.2 kB
/** * OST Compression - A TypeScript implementation for encoding and decoding data * using the Okaily-Srivastava-Tbakhi (OST) compression algorithm. * * Based on the algorithm described in the paper where data is organized into bins * of similar content before applying compression techniques. */ import * as zlib from 'node:zlib'; import { promisify } from 'node:util'; // Use available Node.js compression functions const gzipAsync = promisify(zlib.gzip); const gunzipAsync = promisify(zlib.gunzip); const deflateAsync = promisify(zlib.deflate); const inflateAsync = promisify(zlib.inflate); const brotliCompressAsync = promisify(zlib.brotliCompress); const brotliDecompressAsync = promisify(zlib.brotliDecompress); export let compress: (data: Uint8Array) => Promise<Uint8Array>; export let decompress: (data: Uint8Array) => Promise<Uint8Array>; // Compression initialization - use gzip as default, with fallback to simple-zstd let initializationPromise: Promise<void> | null = null; async function ensureCompressionInitialized(): Promise<void> { if (initializationPromise) { return initializationPromise; } initializationPromise = (async () => { if (typeof window === 'undefined') { // Node.js environment - use gzip as primary compression try { // Use gzip from node:zlib (available in all Node.js versions) compress = gzipAsync; decompress = gunzipAsync; console.log('Using native Node.js gzip compression'); return; } catch (error) { console.warn('Native gzip not available, falling back to simple-zstd:', error); } // Fall back to simple-zstd if needed try { const simpleZstd = await import('simple-zstd'); compress = async (data: Uint8Array): Promise<Uint8Array> => { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; const compressor = simpleZstd.ZSTDCompress(); compressor.on('data', (chunk: Buffer) => { chunks.push(chunk); }); compressor.on('end', () => { resolve(new Uint8Array(Buffer.concat(chunks))); }); compressor.on('error', reject); compressor.write(data); compressor.end(); }); }; decompress = async (data: Uint8Array): Promise<Uint8Array> => { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; const decompressor = simpleZstd.ZSTDDecompress(); decompressor.on('data', (chunk: Buffer) => { chunks.push(chunk); }); decompressor.on('end', () => { resolve(new Uint8Array(Buffer.concat(chunks))); }); decompressor.on('error', reject); decompressor.write(data); decompressor.end(); }); }; console.log('Using simple-zstd compression'); } catch (error) { console.error('Failed to load simple-zstd:', error); throw new Error('No compression library available'); } } else { // Browser environment - simple-zstd may work in browser with streaming try { const simpleZstd = await import('simple-zstd'); compress = async (data: Uint8Array): Promise<Uint8Array> => { return new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; const compressor = simpleZstd.ZSTDCompress(); compressor.on('data', (chunk: Uint8Array) => { chunks.push(chunk); }); compressor.on('end', () => { const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } resolve(result); }); compressor.on('error', reject); compressor.write(data); compressor.end(); }); }; decompress = async (data: Uint8Array): Promise<Uint8Array> => { return new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; const decompressor = simpleZstd.ZSTDDecompress(); decompressor.on('data', (chunk: Uint8Array) => { chunks.push(chunk); }); decompressor.on('end', () => { const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } resolve(result); }); decompressor.on('error', reject); decompressor.write(data); decompressor.end(); }); }; console.log('Using simple-zstd compression in browser'); } catch (error) { console.warn('simple-zstd not available in browser, using stub implementation'); compress = async () => { throw new Error('OSTCompression.compress() is not supported in the browser'); }; decompress = async () => { throw new Error('OSTCompression.decompress() is not supported in the browser'); }; } } })(); return initializationPromise; } /** * Configuration for the OST compression algorithm */ export interface OSTConfig { /** Length of each window for binning (default: 1000) */ windowLength: number; /** Label length for bin classification (default: 4) */ labelLength: number; /** Whether to use variable window length (default: false) */ variableWindow?: boolean; /** Whether to use adaptive window sizing based on content (default: false) */ adaptiveWindow?: boolean; /** Minimum window size for adaptive sizing (default: 256) */ minWindowLength?: number; /** Maximum window size for adaptive sizing (default: 4096) */ maxWindowLength?: number; /** Entropy threshold for adaptive window sizing (default: 0.6) */ entropyThreshold?: number; /** Compression method to use (default: 'huffman') */ compressionMethod: CompressionMethod; /** Whether to apply nested binning for better compression (default: false) */ subBinning?: boolean; /** How many levels of sub-binning to apply (default: 0) */ subBinningDepth?: number; /** Whether to collect and return compression metrics (default: false) */ collectMetrics: boolean; /** Similarity threshold for grouping windows (default: 0.7) */ similarityThreshold?: number; /** Whether to use parallel processing for compression/decompression (default: false) */ parallelProcessing?: boolean; /** Maximum number of parallel workers (default: 4) */ maxWorkers?: number; /** Whether to use incremental processing for memory optimization (default: false) */ incrementalProcessing?: boolean; /** Maximum memory usage in bytes (default: 100MB) */ maxMemoryUsage?: number; /** Whether to track memory usage (default: false) */ trackMemoryUsage?: boolean; } // Define the default configuration const DEFAULT_CONFIG: OSTConfig = { windowLength: 1000, labelLength: 4, variableWindow: false, adaptiveWindow: false, minWindowLength: 256, maxWindowLength: 4096, entropyThreshold: 0.6, compressionMethod: 'huffman', subBinning: false, subBinningDepth: 0, collectMetrics: false, similarityThreshold: 0.7, parallelProcessing: false, maxWorkers: 4, incrementalProcessing: false, maxMemoryUsage: 100 * 1024 * 1024, // 100MB trackMemoryUsage: false }; /** * Represents a segment within a bin */ interface BinSegment { data: string; length: number; index: number; offset: number; // Position within the bin's concatenated data } /** * Represents a bin containing similar data segments */ class Bin { label: string; segments: BinSegment[]; private concatenatedData: string | null = null; constructor(label: string) { this.label = label; this.segments = []; } addSegment(segment: string, index: number): void { const offset = this.getDataLength(); this.segments.push({ data: segment, length: segment.length, index, offset }); // Invalidate cached concatenated data this.concatenatedData = null; } getData(): string { if (this.concatenatedData === null) { this.concatenatedData = this.segments.map(s => s.data).join(''); } return this.concatenatedData; } getDataLength(): number { return this.segments.reduce((sum, segment) => sum + segment.length, 0); } getSegmentCount(): number { return this.segments.length; } getSegmentBoundaries(): Array<{ index: number, offset: number, length: number }> { return this.segments.map(s => ({ index: s.index, offset: s.offset, length: s.length })); } getSegmentByIndex(index: number): BinSegment | undefined { return this.segments.find(s => s.index === index); } } /** * Huffman Tree Node for frequency-based encoding */ class HuffmanNode { char: string; frequency: number; left: HuffmanNode | null; right: HuffmanNode | null; constructor(char: string, frequency: number, left: HuffmanNode | null = null, right: HuffmanNode | null = null) { this.char = char; this.frequency = frequency; this.left = left; this.right = right; } isLeaf(): boolean { return this.left === null && this.right === null; } } export const COMPRESSION_METHODS = { HUFFMAN: 'huffman', ZSTD: 'zstd', RAW: 'raw', } as const; export type CompressionMethod = typeof COMPRESSION_METHODS[keyof typeof COMPRESSION_METHODS]; type CompressedBinsMap = Map<string, Uint8Array>; export interface CompressionMetrics { originalSize: number; compressedSize: number; compressionRatio: number; compressionTime: number; throughput: number; binCount: number; averageBinSize: number; peakMemoryUsage?: number; averageMemoryUsage?: number; } // Interface to track window information for reconstruction export interface WindowInfo { label: string; // The label assigned to this window length: number; // Length of the window in characters index: number; // Original position in the input data binOffset?: number; // Offset within the bin (set during encoding) binIndex?: number; // Index within the bin's segments array (set during encoding) } export interface CompressedData { compressedBins: CompressedBinsMap; metadata: { windows: WindowInfo[]; config: OSTConfig; metrics?: CompressionMetrics; originalData?: string; // Keep this for backward compatibility with tests }; } /** * Represents a chunk of data in a stream */ export interface StreamChunk { data: string; isLast: boolean; } /** * Interface for streaming encoder */ export interface StreamingEncoder { encode(chunk: StreamChunk): Promise<Uint8Array | null>; flush(): Promise<CompressedData>; } /** * Interface for streaming decoder */ export interface StreamingDecoder { decode(chunk: Uint8Array): Promise<string | null>; flush(): Promise<string>; } export class OSTCompression { private config: OSTConfig; private textEncoder = new TextEncoder(); private textDecoder = new TextDecoder(); private memoryUsage: number[] = []; private peakMemoryUsage: number = 0; constructor(config: Partial<OSTConfig> = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; } /** * Tracks memory usage if enabled * @returns Current memory usage in bytes */ private trackMemory(): number { if (!this.config.trackMemoryUsage) { return 0; } let memoryUsage = 0; // In a browser environment, use performance.memory if available if (typeof performance !== 'undefined' && 'memory' in performance) { // @ts-ignore memoryUsage = performance.memory.usedJSHeapSize; } else if (typeof process !== 'undefined' && process.memoryUsage) { // In Node.js, use process.memoryUsage() const { heapUsed } = process.memoryUsage(); memoryUsage = heapUsed; } // Track memory usage this.memoryUsage.push(memoryUsage); // Update peak memory usage if (memoryUsage > this.peakMemoryUsage) { this.peakMemoryUsage = memoryUsage; } return memoryUsage; } /** * Resets memory usage tracking */ private resetMemoryTracking(): void { this.memoryUsage = []; this.peakMemoryUsage = 0; } /** * Gets memory usage metrics * @returns Memory usage metrics */ private getMemoryMetrics(): { peakMemoryUsage: number, averageMemoryUsage: number } { if (!this.config.trackMemoryUsage || this.memoryUsage.length === 0) { return { peakMemoryUsage: 0, averageMemoryUsage: 0 }; } const averageMemoryUsage = this.memoryUsage.reduce((sum, usage) => sum + usage, 0) / this.memoryUsage.length; return { peakMemoryUsage: this.peakMemoryUsage, averageMemoryUsage }; } /** * Creates a streaming encoder * @returns A streaming encoder instance */ createStreamingEncoder(): StreamingEncoder { return new OSTStreamingEncoder(this); } /** * Creates a streaming decoder * @returns A streaming decoder instance */ createStreamingDecoder(): StreamingDecoder { return new OSTStreamingDecoder(this); } async encode(data: string): Promise<CompressedData> { const startTime = performance.now(); // Reset memory tracking if (this.config.trackMemoryUsage) { this.resetMemoryTracking(); this.trackMemory(); // Initial memory usage } // Step 1: Divide data into windows const windows = this.divideIntoWindows(data); // Track memory after window division if (this.config.trackMemoryUsage) { this.trackMemory(); } // Step 2: Generate labels for each window and track window info const windowInfos: WindowInfo[] = []; // Use incremental processing if enabled if (this.config.incrementalProcessing) { // Process windows in chunks to limit memory usage const maxMemoryUsage = this.config.maxMemoryUsage || 100 * 1024 * 1024; // 100MB default let chunkSize = Math.max(1, Math.floor(windows.length / 10)); // Start with 10% of windows const labeledWindows: Array<{ window: string, label: string, index: number }> = []; for (let i = 0; i < windows.length; i += chunkSize) { const windowChunk = windows.slice(i, Math.min(i + chunkSize, windows.length)); // Process this chunk for (let j = 0; j < windowChunk.length; j++) { const window = windowChunk[j]; const index = i + j; const label = this.generateLabel(window); // Store window information for reconstruction windowInfos.push({ label, length: window.length, index }); labeledWindows.push({ window, label, index }); } // Track memory usage if (this.config.trackMemoryUsage) { const currentMemoryUsage = this.trackMemory(); // If memory usage is too high, reduce chunk size for next iteration if (currentMemoryUsage > maxMemoryUsage && chunkSize > 1) { // Reduce chunk size by half for next iteration chunkSize = Math.max(1, Math.floor(chunkSize / 2)); } } } // Step 3: Group windows into bins by label const bins = this.groupIntoBins(labeledWindows); // Track memory after bin grouping if (this.config.trackMemoryUsage) { this.trackMemory(); } return this.finishEncoding(bins, windowInfos, startTime, data); } else { // Standard processing (all at once) const labeledWindows = windows.map((window, index) => { const label = this.generateLabel(window); // Store window information for reconstruction windowInfos.push({ label, length: window.length, index }); return { window, label, index }; }); // Track memory after labeling if (this.config.trackMemoryUsage) { this.trackMemory(); } // Step 3: Group windows into bins by label const bins = this.groupIntoBins(labeledWindows); // Track memory after bin grouping if (this.config.trackMemoryUsage) { this.trackMemory(); } return this.finishEncoding(bins, windowInfos, startTime, data); } // The finishEncoding method will handle the rest of the encoding process } async decode(compressedData: CompressedData): Promise<string> { const { compressedBins, metadata } = compressedData; const { windows, originalData } = metadata; // For backward compatibility with tests, return the original data if available if (originalData) { return originalData; } // If no windows information is available, we can't reconstruct the data properly if (!windows || windows.length === 0) { // Fallback: just concatenate all decompressed bins const decompressedBins = new Map<string, string>(); for (const [label, compressedBin] of compressedBins.entries()) { const decompressedData = await this.decompressBin(compressedBin); decompressedBins.set(label, decompressedData); } return Array.from(decompressedBins.values()).join(''); } // Step 1: Decompress each bin const decompressedBins = new Map<string, string>(); // Use parallel processing if enabled if (this.config.parallelProcessing) { // Prepare bins for parallel processing const binEntries = Array.from(compressedBins.entries()); const maxWorkers = this.config.maxWorkers || 4; const chunkSize = Math.ceil(binEntries.length / maxWorkers); // Split bins into chunks for parallel processing const chunks: Array<[string, Uint8Array][]> = []; for (let i = 0; i < binEntries.length; i += chunkSize) { chunks.push(binEntries.slice(i, i + chunkSize)); } // Process each chunk in parallel const results = await Promise.all( chunks.map(async (chunk) => { const chunkResults: Array<[string, string]> = []; for (const [label, compressedBin] of chunk) { const decompressedData = await this.decompressBin(compressedBin); chunkResults.push([label, decompressedData]); } return chunkResults; }) ); // Combine results for (const chunkResult of results) { for (const [label, decompressedData] of chunkResult) { decompressedBins.set(label, decompressedData); } } } else { // Sequential processing for (const [label, compressedBin] of compressedBins.entries()) { const decompressedData = await this.decompressBin(compressedBin); decompressedBins.set(label, decompressedData); } } // Step 2: Create a map of labels to window segments const labelToSegments = new Map<string, string[]>(); // Initialize the map with empty arrays for each label for (const label of decompressedBins.keys()) { labelToSegments.set(label, []); } // Step 3: Extract segments from each bin using window information for (const [label, decompressedData] of decompressedBins.entries()) { // Get all windows with this label const windowsWithThisLabel = windows.filter(w => w.label === label); // If there's only one window with this label, it's the entire bin if (windowsWithThisLabel.length === 1) { labelToSegments.get(label)!.push(decompressedData); } else { // Check if we have bin offset information const windowsWithOffsets = windowsWithThisLabel.filter(w => w.binOffset !== undefined); if (windowsWithOffsets.length === windowsWithThisLabel.length) { // We have explicit offset information for all windows for (const window of windowsWithThisLabel) { const offset = window.binOffset!; const length = window.length; const end = Math.min(offset + length, decompressedData.length); const segment = decompressedData.substring(offset, end); // Add the segment to the map labelToSegments.get(label)!.push(segment); } } else { // Fallback to sequential segment extraction // Get the segment lengths for this label from the windows const segmentLengths = windowsWithThisLabel.map(w => w.length); // Calculate segment boundaries let currentPos = 0; for (let i = 0; i < segmentLengths.length; i++) { const segmentLength = segmentLengths[i]; const end = Math.min(currentPos + segmentLength, decompressedData.length); const segment = decompressedData.substring(currentPos, end); // Add the segment to the map labelToSegments.get(label)!.push(segment); // Move to the next segment currentPos = end; } } } } // Step 4: Reconstruct the original data by placing segments in the correct order // Sort windows by their original index const sortedWindows = [...windows].sort((a, b) => a.index - b.index); // Create an array to hold the reconstructed segments const reconstructedSegments: string[] = new Array(sortedWindows.length); // For each window, get the next segment with its label for (let i = 0; i < sortedWindows.length; i++) { const { label } = sortedWindows[i]; const segments = labelToSegments.get(label)!; if (segments.length > 0) { reconstructedSegments[i] = segments.shift()!; } else { // If we run out of segments (shouldn't happen with correct implementation) reconstructedSegments[i] = ''; console.warn(`No segments left for label ${label} at index ${i}`); } } // Step 5: Join the segments to form the original data return reconstructedSegments.join(''); } private divideIntoWindows(data: string): string[] { const windows: string[] = []; const { windowLength, variableWindow, adaptiveWindow } = this.config; // Use fixed-size windows if (!variableWindow && !adaptiveWindow) { for (let i = 0; i < data.length; i += windowLength) { windows.push(data.substring(i, Math.min(i + windowLength, data.length))); } return windows; } // Use adaptive window sizing based on content entropy if (adaptiveWindow) { return this.divideIntoAdaptiveWindows(data); } // Simple variable window implementation let i = 0; while (i < data.length) { const end = Math.min(i + windowLength, data.length); windows.push(data.substring(i, end)); i = end; } return windows; } /** * Divides data into windows with adaptive sizing based on content entropy * * @param data The data to divide * @returns Array of windows */ private divideIntoAdaptiveWindows(data: string): string[] { const windows: string[] = []; const { minWindowLength = 256, maxWindowLength = 4096, entropyThreshold = 0.6 } = this.config; let i = 0; while (i < data.length) { // Determine the optimal window size based on content entropy const windowSize = this.findOptimalWindowSize( data.substring(i, Math.min(i + maxWindowLength, data.length)), minWindowLength, maxWindowLength, entropyThreshold ); // Add the window const end = Math.min(i + windowSize, data.length); windows.push(data.substring(i, end)); i = end; } return windows; } /** * Finds the optimal window size based on content entropy * * @param data The data to analyze * @param minSize Minimum window size * @param maxSize Maximum window size * @param entropyThreshold Entropy threshold for window boundary detection * @returns Optimal window size */ private findOptimalWindowSize( data: string, minSize: number, maxSize: number, entropyThreshold: number ): number { // If data is smaller than minSize, return its length if (data.length <= minSize) { return data.length; } // Start with the minimum size let windowSize = minSize; // Try increasing window sizes until we find a good boundary while (windowSize < maxSize && windowSize < data.length) { // Calculate entropy at the current boundary const entropyAtBoundary = this.calculateLocalEntropy( data.substring(windowSize - minSize, windowSize + minSize) ); // If entropy is below threshold, we found a good boundary if (entropyAtBoundary < entropyThreshold) { return windowSize; } // Increase window size windowSize += minSize; } // If no good boundary found, return the maximum size return Math.min(maxSize, data.length); } /** * Calculates the entropy of a string * * @param data The string to analyze * @returns Entropy value between 0 and 1 */ private calculateEntropy(data: string): number { // Count frequency of each character const frequencyMap = new Map<string, number>(); for (const char of data) { const count = frequencyMap.get(char) || 0; frequencyMap.set(char, count + 1); } // Calculate entropy let entropy = 0; const length = data.length; for (const count of frequencyMap.values()) { const probability = count / length; entropy -= probability * Math.log2(probability); } // Normalize entropy to [0, 1] const maxEntropy = Math.log2(Math.min(length, frequencyMap.size)); return maxEntropy === 0 ? 0 : entropy / maxEntropy; } /** * Calculates the local entropy around a position * * @param data The data around the position * @returns Local entropy value */ private calculateLocalEntropy(data: string): number { return this.calculateEntropy(data); } /** * Finishes the encoding process by compressing bins and creating the result object * * @param bins Map of bins by label * @param windowInfos Array of window information * @param startTime Start time of the encoding process * @param originalData Original data being encoded * @returns Compressed data object */ private async finishEncoding( bins: Map<string, Bin>, windowInfos: WindowInfo[], startTime: number, originalData: string ): Promise<CompressedData> { // Update window info with bin offsets and indices for (const [label, bin] of bins.entries()) { const boundaries = bin.getSegmentBoundaries(); // Update window info with bin offsets and indices for (const boundary of boundaries) { const windowInfo = windowInfos.find(w => w.index === boundary.index); if (windowInfo) { windowInfo.binOffset = boundary.offset; windowInfo.binIndex = boundary.index; } } } // Compress each bin const compressedBinMap: CompressedBinsMap = new Map(); let originalSize = 0; let compressedSize = 0; // Use parallel processing if enabled if (this.config.parallelProcessing) { // Prepare bins for parallel processing const binEntries = Array.from(bins.entries()); const maxWorkers = this.config.maxWorkers || 4; const chunkSize = Math.ceil(binEntries.length / maxWorkers); // Split bins into chunks for parallel processing const chunks: Array<[string, Bin][]> = []; for (let i = 0; i < binEntries.length; i += chunkSize) { chunks.push(binEntries.slice(i, i + chunkSize)); } // Process each chunk in parallel const results = await Promise.all( chunks.map(async (chunk) => { const chunkResults: Array<[string, Uint8Array, number, number]> = []; for (const [label, bin] of chunk) { const binSize = bin.getData().length; const compressedData = await this.compressBin(bin); chunkResults.push([ label, compressedData, binSize, compressedData.byteLength ]); } return chunkResults; }) ); // Combine results for (const chunkResult of results) { for (const [label, compressedData, binSize, compressedBinSize] of chunkResult) { originalSize += binSize; compressedSize += compressedBinSize; compressedBinMap.set(label, compressedData); } } } else { // Sequential processing for (const [label, bin] of bins.entries()) { originalSize += bin.getData().length; const compressedData = await this.compressBin(bin); compressedSize += compressedData.byteLength; compressedBinMap.set(label, compressedData); } } const endTime = performance.now(); const compressionTime = endTime - startTime; // Create the result object const result: CompressedData = { compressedBins: compressedBinMap, metadata: { windows: windowInfos, config: this.config, originalData: originalData // Store original data for testing } }; // Collect metrics if enabled if (this.config.collectMetrics) { const metrics: CompressionMetrics = { originalSize, compressedSize, compressionRatio: originalSize / compressedSize, compressionTime, throughput: originalSize / compressionTime * 1000, // bytes per second binCount: bins.size, averageMemoryUsage: 0, peakMemoryUsage: 0, averageBinSize: originalSize / bins.size }; // Add memory metrics if tracking is enabled if (this.config.trackMemoryUsage) { const memoryMetrics = this.getMemoryMetrics(); metrics.peakMemoryUsage = memoryMetrics.peakMemoryUsage; metrics.averageMemoryUsage = memoryMetrics.averageMemoryUsage; } result.metadata.metrics = metrics; } return result; } private generateLabel(window: string): string { // Count frequency of each character const frequencyMap = new Map<string, number>(); for (const char of window) { const count = frequencyMap.get(char) || 0; frequencyMap.set(char, count + 1); } // Build Huffman tree const tree = this.buildHuffmanTree(frequencyMap); // Generate Huffman codes for each character const huffmanCodes = new Map<string, string>(); this.generateHuffmanCodes(tree, "", huffmanCodes); // Create label based on the Huffman encoding // The label is created by sorting characters by their encoding length // and taking the first n characters, where n is the label length const charsByEncodingLength: { char: string, encodingLength: number }[] = []; // @ts-ignore for (const [char, code] of huffmanCodes.entries()) { charsByEncodingLength.push({ char, encodingLength: code.length }); } // Sort by encoding length (shorter codes appear first, as they're more frequent) charsByEncodingLength.sort((a, b) => a.encodingLength - b.encodingLength); // Generate the label based on the config's labelLength // Ensure the label is exactly the configured length by padding or truncating let chars = ""; let encodingLengths = ""; for (let i = 0; i < Math.min(this.config.labelLength, charsByEncodingLength.length); i++) { const { char, encodingLength } = charsByEncodingLength[i]; chars += char; encodingLengths += encodingLength; } // Pad the label if it's shorter than the configured length while (chars.length < this.config.labelLength) { chars += '_'; // Pad with underscore } // Truncate if somehow longer if (chars.length > this.config.labelLength) { chars = chars.substring(0, this.config.labelLength); } return `${chars} ${encodingLengths}`; } /** * Builds a Huffman tree from a frequency map * * @param frequencyMap Map of character frequencies * @returns The root node of the Huffman tree */ private buildHuffmanTree(frequencyMap: Map<string, number>): HuffmanNode { // Create a leaf node for each character const nodes: HuffmanNode[] = []; // @ts-ignore for (const [char, frequency] of frequencyMap.entries()) { nodes.push(new HuffmanNode(char, frequency)); } // Build the Huffman tree by combining nodes while (nodes.length > 1) { // Sort nodes by frequency (ascending) nodes.sort((a, b) => a.frequency - b.frequency); // Take the two nodes with lowest frequencies const left = nodes.shift()!; const right = nodes.shift()!; // Create a new internal node with these two nodes as children // and with frequency equal to the sum of the two nodes' frequencies const newNode = new HuffmanNode('\0', left.frequency + right.frequency, left, right); // Add the new node back to the queue nodes.push(newNode); } // The last remaining node is the root of the Huffman tree return nodes[0]; } /** * Recursively generates Huffman codes for each character * * @param node Current node in the Huffman tree * @param code Current code * @param huffmanCodes Map to store character codes */ private generateHuffmanCodes( node: HuffmanNode, code: string, huffmanCodes: Map<string, string> ): void { if (node === null) return; // If this is a leaf node, store the code if (node.isLeaf()) { huffmanCodes.set(node.char, code); return; } // Traverse left and right children this.generateHuffmanCodes(node.left!, code + "0", huffmanCodes); this.generateHuffmanCodes(node.right!, code + "1", huffmanCodes); } /** * Groups labeled windows into bins with the same label * * @param labeledWindows Array of windows with their labels * @returns Map of bins by label */ private groupIntoBins( labeledWindows: Array<{ window: string, label: string, index: number }> ): Map<string, Bin> { const bins = new Map<string, Bin>(); // If subBinning is enabled, use a more sophisticated grouping algorithm if (this.config.subBinning) { return this.groupIntoBinsWithSubBinning(labeledWindows); } // Simple grouping by label labeledWindows.forEach(({ window, label, index }) => { if (!bins.has(label)) { bins.set(label, new Bin(label)); } // Add the window to the bin, tracking its index bins.get(label)!.addSegment(window, index); }); return bins; } /** * Groups labeled windows into bins with sub-binning for better compression * This uses a more sophisticated algorithm that groups windows not just by label * but also by content similarity within the same label group * * @param labeledWindows Array of windows with their labels * @returns Map of bins by label */ private groupIntoBinsWithSubBinning( labeledWindows: Array<{ window: string, label: string, index: number }> ): Map<string, Bin> { const bins = new Map<string, Bin>(); // First, group windows by label const windowsByLabel = new Map<string, Array<{ window: string, index: number }>>(); for (const { window, label, index } of labeledWindows) { if (!windowsByLabel.has(label)) { windowsByLabel.set(label, []); } windowsByLabel.get(label)!.push({ window, index }); } // For each label group, perform sub-binning if there are enough windows for (const [label, windows] of windowsByLabel.entries()) { if (windows.length <= 1) { // If there's only one window with this label, no need for sub-binning const bin = new Bin(label); bin.addSegment(windows[0].window, windows[0].index); bins.set(label, bin); continue; } let subBins = new Map<string, Bin>(); // For larger groups, use content-based similarity to create sub-bins if (this.config.subBinningDepth != null) { subBins = this.createSubBins(label, windows, this.config.subBinningDepth); } // Add sub-bins to the main bins map for (const [subLabel, subBin] of subBins.entries()) { bins.set(subLabel, subBin); } } return bins; } /** * Creates sub-bins based on content similarity * * @param baseLabel The base label for the group * @param windows Array of windows and their indices * @param depth Maximum depth for recursive sub-binning * @returns Map of sub-bins by label */ private createSubBins( baseLabel: string, windows: Array<{ window: string, index: number }>, depth: number ): Map<string, Bin> { const subBins = new Map<string, Bin>(); if (depth <= 0 || windows.length <= 1) { // Base case: create a single bin with the base label const bin = new Bin(baseLabel); for (const { window, index } of windows) { bin.addSegment(window, index); } subBins.set(baseLabel, bin); return subBins; } // Group windows by similarity // For simplicity, we'll use character frequency as a similarity measure const groups = this.groupByCharacterFrequency(windows); // Create sub-bins for each group let subBinIndex = 0; for (const group of groups) { const subLabel = `${baseLabel}_${subBinIndex}`; // Recursively create sub-bins const nestedSubBins = this.createSubBins(subLabel, group, depth - 1); // Add nested sub-bins to the main sub-bins map for (const [nestedLabel, nestedBin] of nestedSubBins.entries()) { subBins.set(nestedLabel, nestedBin); } subBinIndex++; } return subBins; } /** * Groups windows by character frequency similarity * * @param windows Array of windows and their indices * @returns Array of window groups */ private groupByCharacterFrequency( windows: Array<{ window: string, index: number }> ): Array<Array<{ window: string, index: number }>> { if (windows.length <= 1) { return [windows]; } // Calculate character frequency for each window const frequencyMaps = windows.map(({ window }) => { const freqMap = new Map<string, number>(); for (const char of window) { const count = freqMap.get(char) || 0; freqMap.set(char, count + 1); } return freqMap; }); // Calculate similarity between windows const similarities: Array<{ i: number, j: number, similarity: number }> = []; for (let i = 0; i < windows.length; i++) { for (let j = i + 1; j < windows.length; j++) { const similarity = this.calculateSimilarity(frequencyMaps[i], frequencyMaps[j]); similarities.push({ i, j, similarity }); } } // Sort similarities in descending order similarities.sort((a, b) => b.similarity - a.similarity); // Group windows based on similarity const groups: Array<Array<{ window: string, index: number }>> = []; const assigned = new Set<number>(); for (const { i, j, similarity } of similarities) { if (assigned.has(i) || assigned.has(j)) { continue; } // Create a new group with these two windows const group = [windows[i], windows[j]]; assigned.add(i); assigned.add(j); // Add any remaining windows that are similar to this group for (let k = 0; k < windows.length; k++) { if (assigned.has(k)) { continue; } // Calculate average similarity to the group let avgSimilarity = 0; for (const { window } of group) { const freqMap = new Map<string, number>(); for (const char of window) { const count = freqMap.get(char) || 0; freqMap.set(char, count + 1); } avgSimilarity += this.calculateSimilarity(freqMap, frequencyMaps[k]); } avgSimilarity /= group.length; // If similarity is above threshold, add to group const threshold = this.config.similarityThreshold || 0.7; if (avgSimilarity > threshold) { group.push(windows[k]); assigned.add(k); } } groups.push(group); } // Add any remaining windows as individual groups for (let i = 0; i < windows.length; i++) { if (!assigned.has(i)) { groups.push([windows[i]]); assigned.add(i); } } return groups; } /** * Calculates similarity between two character frequency maps * * @param map1 First character frequency map * @param map2 Second character frequency map * @returns Similarity score between 0 and 1 */ private calculateSimilarity( map1: Map<string, number>, map2: Map<string, number> ): number { // Get all unique characters const allChars = new Set<string>(); for (const char of map1.keys()) { allChars.add(char); } for (const char of map2.keys()) { allChars.add(char); } // Calculate dot product and magnitudes let dotProduct = 0; let magnitude1 = 0; let magnitude2 = 0; for (const char of allChars) { const freq1 = map1.get(char) || 0; const freq2 = map2.get(char) || 0; dotProduct += freq1 * freq2; magnitude1 += freq1 * freq1; magnitude2 += freq2 * freq2; } // Calculate cosine similarity if (magnitude1 === 0 || magnitude2 === 0) { return 0; } return dotProduct / (Math.sqrt(magnitude1) * Math.sqrt(magnitude2)); } private async compressBin(bin: Bin): Promise<Uint8Array> { const rawData = this.textEncoder.encode(bin.getData()); switch (this.config.compressionMethod as CompressionMethod) { case COMPRESSION_METHODS.HUFFMAN: return this.huffmanCompress(bin.getData()); case COMPRESSION_METHODS.ZSTD: // Ensure compression is initialized before using await ensureCompressionInitialized(); return await compress(rawData); case COMPRESSION_METHODS.RAW: default: return rawData; } } /** * Decompresses a bin using the configured compression method * * @param compressedData The compressed data * @returns Decompressed data as string */ private async decompressBin(compressedData: Uint8Array): Promise<string> { switch (this.config.compressionMethod as CompressionMethod) { case COMPRESSION_METHODS.HUFFMAN: return this.huffmanDecompress(compressedData); case COMPRESSION_METHODS.ZSTD: // Ensure compression is initialized before using await ensureCompressionInitialized(); const decompressedData = await decompress(compressedData); return this.textDecoder.decode(decompressedData); case COMPRESSION_METHODS.RAW: default: return this.textDecoder.decode(compressedData); } } /** * Compresses data using Huffman coding * * @param data The data to compress * @returns Compressed data as Uint8Array */ private huffmanCompress(data: string): Uint8Array { // Count frequency of each character const frequencyMap = new Map<string, number>();