UNPKG

@brianpugh/tamp

Version:

Tamp compression library for JavaScript/TypeScript using WebAssembly

912 lines (782 loc) 31.2 kB
/** * Tamp WebAssembly JavaScript Wrapper * High-level JavaScript API for Tamp compression/decompression */ import TampModule from './tamp-module.mjs'; // Error codes from C library const TAMP_ERROR = -1; const TAMP_EXCESS_BITS = -2; const TAMP_INVALID_CONF = -3; class TampError extends Error { constructor(code, message, details = {}) { super(message); this.name = 'TampError'; this.code = code; this.details = details; } } class ExcessBitsError extends TampError { constructor(message = 'Provided data has more bits than expected literal bits', details = {}) { super(-2, message, details); this.name = 'ExcessBitsError'; } } class CompressionError extends TampError { constructor(message = 'Compression operation failed', details = {}) { super(-1, message, details); this.name = 'CompressionError'; } } class DecompressionError extends TampError { constructor(message = 'Decompression operation failed', details = {}) { super(-1, message, details); this.name = 'DecompressionError'; } } /** * WebAssembly module singleton with proper initialization */ class WasmModuleManager { #module = null; #initPromise = null; async initialize() { if (!this.#initPromise) { this.#initPromise = TampModule(); } this.#module = await this.#initPromise; return this.#module; } get module() { return this.#module; } get isInitialized() { return this.#module !== null; } } const wasmManager = new WasmModuleManager(); async function initializeWasm() { return await wasmManager.initialize(); } function throwOnError(result, operation) { if (result < 0) { const details = { operation, code: result }; switch (result) { case TAMP_EXCESS_BITS: throw new ExcessBitsError(`${operation}: Symbol has more bits than configured literal size`, details); case TAMP_INVALID_CONF: throw new RangeError(`${operation}: Invalid configuration parameters`); case TAMP_ERROR: default: if (operation.toLowerCase().includes('compress')) { throw new CompressionError(`${operation}: Operation failed`, details); } else if (operation.toLowerCase().includes('decompress')) { throw new DecompressionError(`${operation}: Operation failed`, details); } else { throw new TampError(result, `${operation}: Unknown error`, details); } } } return result; } /** * Tamp Compressor class for streaming compression */ export class TampCompressor { constructor(options = {}) { this.options = { window: 10, literal: 8, dictionary: null, lazy_matching: false, ...options, }; this.initialized = false; this.compressorPtr = null; this.windowPtr = null; this.module = null; this._initPromise = null; } async initialize() { if (!this._initPromise) { this._initPromise = this._doInitialize(); } return this._initPromise; } async _doInitialize() { if (this.initialized) return; this.module = await initializeWasm(); const windowSize = 1 << this.options.window; // Allocate memory for compressor struct, config struct, and window buffer in single allocation const totalSize = 64 + 4 + windowSize; // TampCompressor struct + config struct + window buffer this.compressorPtr = this.module._malloc(totalSize); if (this.compressorPtr === 0) { throw new RangeError(`Failed to allocate memory for compressor (${totalSize} bytes)`); } const confPtr = this.compressorPtr + 64; this.windowPtr = confPtr + 4; // Initialize dictionary if (this.options.dictionary) { // Use provided custom dictionary const dictData = new Uint8Array(this.options.dictionary); if (dictData.length !== windowSize) { throw new RangeError(`Dictionary size (${dictData.length}) must match window size (${windowSize})`); } // Copy dictionary data byte by byte using setValue for (let i = 0; i < dictData.length; i++) { this.module.setValue(this.windowPtr + i, dictData[i], 'i8'); } } else { // Use default dictionary initialization this.module.ccall('tamp_initialize_dictionary', null, ['number', 'number'], [this.windowPtr, windowSize]); } // Create configuration struct (already allocated as part of single allocation) try { const confValue = (this.options.window & 0xf) | ((this.options.literal & 0xf) << 4) | ((this.options.dictionary ? 1 : 0) << 8) | ((this.options.lazy_matching ? 1 : 0) << 9); this.module.setValue(confPtr, confValue, 'i32'); // Initialize compressor const result = this.module.ccall( 'tamp_compressor_init', 'number', ['number', 'number', 'number'], [this.compressorPtr, confPtr, this.windowPtr] ); throwOnError(result, 'Compressor initialization'); } catch (error) { this.module._free(this.compressorPtr); // Frees struct, window buffer, and config this.compressorPtr = null; this.windowPtr = null; throw error; } this.initialized = true; } /** * Compress data chunk * @param {Uint8Array} input - Input data to compress * @param {Object} [options] - Options object * @param {Function} [options.onPoll] - Progress callback * @param {AbortSignal} [options.signal] - AbortSignal for cancellation * @param {number} [options.pollIntervalMs=100] - Minimum interval between progress callbacks in milliseconds * @param {number} [options.pollIntervalBytes=65536] - Minimum bytes processed between progress callbacks * @returns {Promise<Uint8Array>} - Compressed output */ async compress(input, options = {}) { const progressCallback = options.onPoll; await this.initialize(); console.log(`Starting compression of ${input.length} bytes...`); const compressionStartTime = performance.now(); const CHUNK_SIZE = 1 << 20; const outputChunks = []; // Extract options const { signal, pollIntervalMs = 100, pollIntervalBytes = 65536 } = options; // Progress tracking const startTime = performance.now(); let lastProgressTime = startTime; let lastProgressBytes = 0; let totalChunksProcessed = 0; // Check for cancellation const checkAborted = () => { if (signal?.aborted) { throw new CompressionError('Compression was aborted', { aborted: true, reason: signal.reason, }); } }; // Single allocation for input buffer + output buffer + four 4-byte pointers const totalAllocSize = CHUNK_SIZE + CHUNK_SIZE + 16; const basePtr = this.module._malloc(totalAllocSize); if (basePtr === 0) { throw new RangeError(`Failed to allocate memory (${totalAllocSize} bytes)`); } const inputPtr = basePtr; const outputPtr = inputPtr + CHUNK_SIZE; const outputSizePtr = outputPtr + CHUNK_SIZE; const inputConsumedPtr = outputSizePtr + 4; const sinkConsumedPtr = inputConsumedPtr + 4; const pollOutputSizePtr = sinkConsumedPtr + 4; // No need for callback function pointer setup since we're calling JS callback directly try { checkAborted(); // Check for cancellation before we start let inputRemaining = input.length; let inputOffset = 0; while (inputRemaining > 0) { checkAborted(); // Check for cancellation at start of each chunk const currentInputSize = Math.min(inputRemaining, CHUNK_SIZE); // Copy current input chunk to WASM memory for (let i = 0; i < currentInputSize; i++) { this.module.setValue(inputPtr + i, input[inputOffset + i], 'i8'); } // Use lower-level API instead of tamp_compressor_compress_cb // This is a translation of the C implementation let chunkInputPtr = inputPtr; let chunkInputSize = currentInputSize; let chunkInputConsumed = 0; let chunkOutputWritten = 0; // Main compression loop - equivalent to the while loop in tamp_compressor_compress_cb while (chunkInputSize > 0 && CHUNK_SIZE - chunkOutputWritten > 0) { // Sink data into compressor's internal buffer this.module.ccall( 'tamp_compressor_sink', null, ['number', 'number', 'number', 'number'], [this.compressorPtr, chunkInputPtr, chunkInputSize, sinkConsumedPtr] ); const sinkConsumed = this.module.getValue(sinkConsumedPtr, 'i32'); chunkInputPtr += sinkConsumed; chunkInputSize -= sinkConsumed; chunkInputConsumed += sinkConsumed; // Check if compressor internal buffer is full const inputBufferIsFull = this.module.ccall( 'tamp_compressor_full', 'number', ['number'], [this.compressorPtr] ); if (inputBufferIsFull) { // Perform 1 compression cycle. const pollResult = this.module.ccall( 'tamp_compressor_poll', 'number', ['number', 'number', 'number', 'number'], [this.compressorPtr, outputPtr + chunkOutputWritten, CHUNK_SIZE - chunkOutputWritten, pollOutputSizePtr] ); if (pollResult !== 0) { throwOnError(pollResult, 'Compression poll'); } const pollOutputSize = this.module.getValue(pollOutputSizePtr, 'i32'); chunkOutputWritten += pollOutputSize; // Call progress callback if provided (after successful compression) // Use throttling to reduce callback overhead if (progressCallback && typeof progressCallback === 'function') { const currentTime = performance.now(); const bytesProcessed = inputOffset + chunkInputConsumed; const timeSinceLastCallback = currentTime - lastProgressTime; const bytesSinceLastCallback = bytesProcessed - lastProgressBytes; // Only call callback if enough time has passed OR enough bytes processed const shouldCallback = timeSinceLastCallback >= pollIntervalMs || bytesSinceLastCallback >= pollIntervalBytes || bytesProcessed === input.length; // Always call on completion if (shouldCallback) { checkAborted(); // Check for cancellation before callback const elapsedSeconds = (currentTime - startTime) / 1000; const bytesPerSecond = elapsedSeconds > 0 ? bytesProcessed / elapsedSeconds : 0; const progressPercent = input.length > 0 ? (bytesProcessed / input.length) * 100 : 0; const remainingBytes = input.length - bytesProcessed; const estimatedTimeRemaining = bytesPerSecond > 0 ? remainingBytes / bytesPerSecond : 0; // Progress callback: (progressInfo) => void | boolean const progressInfo = { bytesProcessed, totalBytes: input.length, percent: progressPercent, bytesPerSecond: Math.round(bytesPerSecond), estimatedTimeRemaining, chunksProcessed: totalChunksProcessed, elapsedTime: elapsedSeconds, chunkSize: pollOutputSize, outputSize: chunkOutputWritten, }; await Promise.resolve(progressCallback(progressInfo)); lastProgressTime = currentTime; lastProgressBytes = bytesProcessed; } } // Check for cancellation after callback (in case callback triggered abort) checkAborted(); totalChunksProcessed++; } } // Set the final values this.module.setValue(outputSizePtr, chunkOutputWritten, 'i32'); this.module.setValue(inputConsumedPtr, chunkInputConsumed, 'i32'); // Copy output chunk if (chunkOutputWritten > 0) { const outputChunk = new Uint8Array(chunkOutputWritten); for (let i = 0; i < chunkOutputWritten; i++) { outputChunk[i] = this.module.getValue(outputPtr + i, 'i8'); } outputChunks.push(outputChunk); } // Update remaining input inputOffset += chunkInputConsumed; inputRemaining -= chunkInputConsumed; // Safety check: if no input was consumed, break to avoid infinite loop if (chunkInputConsumed === 0 && inputRemaining > 0) { throw new CompressionError('No input consumed in compression loop', { inputRemaining, inputConsumed: chunkInputConsumed, }); } } // Concatenate all output chunks const totalSize = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0); const result = new Uint8Array(totalSize); let offset = 0; for (const chunk of outputChunks) { result.set(chunk, offset); offset += chunk.length; } const compressionEndTime = performance.now(); const compressionTime = compressionEndTime - compressionStartTime; console.log( `Compression completed in ${compressionTime.toFixed(2)}ms. Compressed ${ input.length } bytes to ${totalSize} bytes (${((1 - totalSize / input.length) * 100).toFixed(1)}% reduction)` ); return result; } finally { this.module._free(basePtr); } } /** * Flush any remaining data and finalize compression * @param {boolean} [write_token=false] - Whether to write a token during flush * @returns {Promise<Uint8Array>} - Final compressed output */ async flush(write_token = false) { await this.initialize(); // Single allocation for output buffer (32 bytes) + output size pointer (4 bytes) const totalAllocSize = 32 + 4; const basePtr = this.module._malloc(totalAllocSize); if (basePtr === 0) { throw new RangeError(`Failed to allocate flush memory (${totalAllocSize} bytes)`); } const outputPtr = basePtr; const outputSizePtr = basePtr + 32; try { const result = this.module.ccall( 'tamp_compressor_flush', 'number', ['number', 'number', 'number', 'number', 'number'], [this.compressorPtr, outputPtr, 32, outputSizePtr, write_token ? 1 : 0] ); throwOnError(result, 'Flush'); const outputSize = this.module.getValue(outputSizePtr, 'i32'); const flushed = new Uint8Array(outputSize); // Copy output data byte by byte using getValue for (let i = 0; i < outputSize; i++) { flushed[i] = this.module.getValue(outputPtr + i, 'i8'); } return flushed; } finally { this.module._free(basePtr); } } /** * Clean up allocated memory * @returns {void} */ destroy() { if (this.compressorPtr) { this.module._free(this.compressorPtr); // Frees compressor struct, config struct, and window buffer this.compressorPtr = null; this.windowPtr = null; // Don't free separately - part of same allocation } this.initialized = false; } } /** * Tamp Decompressor class for streaming decompression */ export class TampDecompressor { constructor(options = {}) { this.options = { window: 10, literal: 8, dictionary: null, lazy_matching: false, ...options, }; this.initialized = false; this.decompressorPtr = null; this.windowPtr = null; this.module = null; this._initPromise = null; this.initialPtr = null; this.confPtr = null; this.inputConsumedPtr = null; this.headerInputPtr = null; // State tracking for streaming this.headerRead = false; this.decompressorInitialized = false; this.pendingInput = new Uint8Array(0); } async initialize() { if (!this._initPromise) { this._initPromise = this._doInitialize(); } return this._initPromise; } async _doInitialize() { if (this.initialized) return; this.module = await initializeWasm(); // Allocate memory for header processing: conf(16) + inputConsumed(4) + headerInput(1) = 21 bytes const initialSize = 16 + 4 + 1; this.initialPtr = this.module._malloc(initialSize); if (this.initialPtr === 0) { throw new RangeError(`Failed to allocate initial memory (${initialSize} bytes)`); } this.confPtr = this.initialPtr; this.inputConsumedPtr = this.initialPtr + 16; this.headerInputPtr = this.inputConsumedPtr + 4; this.initialized = true; } /** * Read and process the header from compressed data. * @param {Uint8Array} input - Input data containing the header * @returns {Promise<number>} - Number of bytes consumed from input */ async _readHeader(input) { if (this.headerRead) { return 0; // Header already read } if (input.length === 0) { return 0; // No data to process } // Copy first byte for header reading this.module.setValue(this.headerInputPtr, input[0], 'i8'); // Read header to get configuration const headerResult = this.module.ccall( 'tamp_decompressor_read_header', 'number', ['number', 'number', 'number', 'number'], [this.confPtr, this.headerInputPtr, 1, this.inputConsumedPtr] ); throwOnError(headerResult, 'Header reading'); // Extract configuration values from the header const confValue = this.module.getValue(this.confPtr, 'i32'); const headerWindow = confValue & 0xf; // const headerLiteral = (confValue >> 4) & 0xf; // Currently unused const headerUsesCustomDict = (confValue >> 8) & 1; if (headerUsesCustomDict && !this.options.dictionary) { throw new TypeError('Compressed data requires custom dictionary but none provided'); } if (!headerUsesCustomDict && this.options.dictionary) { throw new TypeError('Compressed data does not use custom dictionary but one was provided'); } // Use header-derived configuration for window size const windowSize = 1 << headerWindow; // Allocate memory for decompressor struct and window buffer in single allocation const decompressorTotalSize = 32 + windowSize; // TampDecompressor struct + window buffer this.decompressorPtr = this.module._malloc(decompressorTotalSize); if (this.decompressorPtr === 0) { throw new RangeError(`Failed to allocate memory for decompressor (${decompressorTotalSize} bytes)`); } this.windowPtr = this.decompressorPtr + 32; // Window buffer starts after struct // Initialize dictionary based on header information if (headerUsesCustomDict) { // Use provided custom dictionary const dictData = new Uint8Array(this.options.dictionary); if (dictData.length !== windowSize) { throw new RangeError(`Dictionary size (${dictData.length}) must match header window size (${windowSize})`); } // Copy dictionary data byte by byte using setValue for (let i = 0; i < dictData.length; i++) { this.module.setValue(this.windowPtr + i, dictData[i], 'i8'); } } else { // Use default dictionary initialization this.module.ccall('tamp_initialize_dictionary', null, ['number', 'number'], [this.windowPtr, windowSize]); } // Initialize decompressor with header-derived configuration try { const result = this.module.ccall( 'tamp_decompressor_init', 'number', ['number', 'number', 'number'], [this.decompressorPtr, this.confPtr, this.windowPtr] ); throwOnError(result, 'Decompressor initialization'); this.decompressorInitialized = true; } catch (error) { this.module._free(this.decompressorPtr); // Frees both struct and window buffer this.decompressorPtr = null; this.windowPtr = null; throw error; } this.headerRead = true; return 1; // Header consumed 1 byte } /** * Decompress data in incremental chunks. * @param {Uint8Array} input - Compressed input data * @returns {Promise<Uint8Array>} - Decompressed output */ async decompress(input) { await this.initialize(); // Combine any pending input with new input const combinedInput = new Uint8Array(this.pendingInput.length + input.length); combinedInput.set(this.pendingInput); combinedInput.set(input, this.pendingInput.length); let inputOffset = 0; // Read header if not already done if (!this.headerRead) { const headerBytesConsumed = await this._readHeader(combinedInput); inputOffset += headerBytesConsumed; // If we only have header data, store remaining for next call if (inputOffset >= combinedInput.length) { this.pendingInput = new Uint8Array(0); return new Uint8Array(0); } } // Process remaining data as payload const payloadData = combinedInput.slice(inputOffset); if (payloadData.length === 0) { this.pendingInput = new Uint8Array(0); return new Uint8Array(0); } const CHUNK_SIZE = 1 << 22; const outputChunks = []; // Single allocation for output size pointer (4 bytes) + input consumed pointer (4 bytes) + input buffer + output buffer // Put fixed-size pointers first to avoid alignment issues const totalAllocSize = 8 + payloadData.length + CHUNK_SIZE; const basePtr = this.module._malloc(totalAllocSize); if (basePtr === 0) { throw new RangeError(`Failed to allocate decompress memory (${totalAllocSize} bytes)`); } const outputSizePtr = basePtr; const inputConsumedPtr = outputSizePtr + 4; const inputPtr = inputConsumedPtr + 4; const outputPtr = inputPtr + payloadData.length; try { // Copy payload data to WASM memory for (let i = 0; i < payloadData.length; i++) { this.module.setValue(inputPtr + i, payloadData[i], 'i8'); } let payloadOffset = 0; let payloadSize = payloadData.length; // Process data incrementally while (payloadSize > 0) { const result = this.module.ccall( 'tamp_decompressor_decompress_cb', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'], [ this.decompressorPtr, outputPtr, CHUNK_SIZE, outputSizePtr, inputPtr + payloadOffset, payloadSize, inputConsumedPtr, 0, // NULL callback 0, // NULL user_data ] ); const outputSize = this.module.getValue(outputSizePtr, 'i32'); const inputConsumed = this.module.getValue(inputConsumedPtr, 'i32'); payloadSize -= inputConsumed; payloadOffset += inputConsumed; // Copy output chunk if any data was produced if (outputSize > 0) { const outputChunk = new Uint8Array(outputSize); for (let i = 0; i < outputSize; i++) { outputChunk[i] = this.module.getValue(outputPtr + i, 'i8'); } outputChunks.push(outputChunk); } if (result === 2) { // TAMP_INPUT_EXHAUSTED - store remaining data for next call if (payloadSize > 0) { this.pendingInput = payloadData.slice(payloadOffset); } else { this.pendingInput = new Uint8Array(0); } break; } else if (result < 0) { throwOnError(result, 'Decompression'); } // If no more input to process, break if (payloadSize === 0) { this.pendingInput = new Uint8Array(0); break; } } if (outputChunks.length === 0) { return new Uint8Array(0); } else if (outputChunks.length === 1) { return outputChunks[0]; } else { const totalSize = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0); const result = new Uint8Array(totalSize); let offset = 0; for (const chunk of outputChunks) { result.set(chunk, offset); offset += chunk.length; } return result; } } finally { this.module._free(basePtr); } } /** * Clean up allocated memory * @returns {void} */ destroy() { if (this.decompressorPtr) { this.module._free(this.decompressorPtr); // Frees both struct and window buffer this.decompressorPtr = null; this.windowPtr = null; // Don't free separately - part of same allocation } if (this.initialPtr) { this.module._free(this.initialPtr); // Frees conf, inputConsumed, and headerInput this.initialPtr = null; this.confPtr = null; this.inputConsumedPtr = null; this.headerInputPtr = null; } this.initialized = false; this.headerRead = false; this.decompressorInitialized = false; this.pendingInput = new Uint8Array(0); } } /** * One-shot compression function * @param {Uint8Array} data - Data to compress * @param {import('./tamp.d.ts').TampOptions|Object} [options] - Compression options * @param {Function} [options.onPoll] - Progress callback with rich progress info * @param {AbortSignal} [options.signal] - AbortSignal for cancellation * @param {number} [options.pollIntervalMs=100] - Minimum interval between progress callbacks in milliseconds * @param {number} [options.pollIntervalBytes=65536] - Minimum bytes processed between progress callbacks * @returns {Promise<Uint8Array>} - Compressed data */ export async function compress(data, options = {}) { // Separate compression options from callback options const compressionOptions = {}; const callbackOptions = {}; // Extract compression-specific options const { window, literal, dictionary, lazy_matching, onPoll, signal, pollIntervalMs, pollIntervalBytes } = options; if (window !== undefined) compressionOptions.window = window; if (literal !== undefined) compressionOptions.literal = literal; if (dictionary !== undefined) compressionOptions.dictionary = dictionary; if (lazy_matching !== undefined) compressionOptions.lazy_matching = lazy_matching; // Extract callback options callbackOptions.onPoll = onPoll; callbackOptions.signal = signal; if (pollIntervalMs !== undefined) callbackOptions.pollIntervalMs = pollIntervalMs; if (pollIntervalBytes !== undefined) callbackOptions.pollIntervalBytes = pollIntervalBytes; const compressor = new TampCompressor(compressionOptions); try { const compressed = await compressor.compress(data, callbackOptions); const flushed = await compressor.flush(); // Concatenate compressed data and flush output const result = new Uint8Array(compressed.length + flushed.length); result.set(compressed); result.set(flushed, compressed.length); return result; } finally { compressor.destroy(); } } /** * One-shot decompression function * @param {Uint8Array} data - Compressed data to decompress * @param {import('./tamp.d.ts').TampOptions} [options] - Decompression options * @returns {Promise<Uint8Array>} - Decompressed data */ export async function decompress(data, options = {}) { const decompressor = new TampDecompressor(options); try { return await decompressor.decompress(data); } finally { decompressor.destroy(); } } /** * Initialize the WebAssembly module * @returns {Promise<void>} */ export async function initialize() { await initializeWasm(); } /** * Utility function to initialize a dictionary buffer with default values * @param {number} size - Size of the dictionary buffer (must be power of 2) * @returns {Uint8Array} - Initialized dictionary buffer */ export async function initializeDictionary(size) { if (!Number.isInteger(size) || size <= 0 || (size & (size - 1)) !== 0) { throw new RangeError('Dictionary size must be a positive power of 2'); } const module = await initializeWasm(); const bufferPtr = module._malloc(size); if (bufferPtr === 0) { throw new RangeError(`Failed to allocate dictionary buffer (${size} bytes)`); } try { // Call the C function to initialize the dictionary module.ccall('tamp_initialize_dictionary', null, ['number', 'number'], [bufferPtr, size]); // Copy the initialized data back to JavaScript byte by byte const dictionary = new Uint8Array(size); for (let i = 0; i < size; i++) { dictionary[i] = module.getValue(bufferPtr + i, 'i8'); } return dictionary; } finally { module._free(bufferPtr); } } /** * Compute the minimum pattern size for given window and literal parameters * @param {number} window - Number of window bits (8-15) * @param {number} literal - Number of literal bits (5-8) * @returns {number} - Minimum pattern size in bytes (2 or 3) */ export async function computeMinPatternSize(window, literal) { if (!Number.isInteger(window) || window < 8 || window > 15) { throw new RangeError('Window must be an integer between 8 and 15'); } if (!Number.isInteger(literal) || literal < 5 || literal > 8) { throw new RangeError('Literal must be an integer between 5 and 8'); } const module = await initializeWasm(); return module.ccall('tamp_compute_min_pattern_size', 'number', ['number', 'number'], [window, literal]); } /** * Compress text string to bytes * @param {string} text - Text string to compress * @param {import('./tamp.d.ts').TampOptions|Object} [options] - Compression options * @param {Function} [options.onPoll] - Progress callback with rich progress info * @param {AbortSignal} [options.signal] - AbortSignal for cancellation * @param {number} [options.pollIntervalMs=100] - Minimum interval between progress callbacks in milliseconds * @param {number} [options.pollIntervalBytes=65536] - Minimum bytes processed between progress callbacks * @returns {Promise<Uint8Array>} - Compressed data */ export async function compressText(text, options = {}) { const data = new TextEncoder().encode(text); return await compress(data, options); } /** * Decompress bytes to text string * @param {Uint8Array} data - Compressed data to decompress * @param {import('./tamp.d.ts').TampOptions} [options] - Decompression options * @param {string} [encoding='utf-8'] - Text encoding to use * @returns {Promise<string>} - Decompressed text */ export async function decompressText(data, options = {}, encoding = 'utf-8') { const decompressed = await decompress(data, options); return new TextDecoder(encoding).decode(decompressed); } /** * Automatic resource management helper * @param {TampCompressor|TampDecompressor} resource - Resource to manage * @param {(resource: TampCompressor|TampDecompressor) => Promise<any>|any} fn - Function to execute with the resource * @returns {Promise<any>} - Result of the function */ export async function using(resource, fn) { try { return await fn(resource); } finally { if (resource && typeof resource.destroy === 'function') { resource.destroy(); } } } export { TampError, ExcessBitsError, CompressionError, DecompressionError };