UNPKG

@lyleunderwood/streaming-zipper

Version:

Memory-efficient streaming ZIP creation with automatic backpressure control. Supports parallel reading + sequential writing for both Web Streams and Node.js streams with ZIP64 support.

4 lines 126 kB
{ "version": 3, "sources": ["../src/index.ts", "../src/zip-format.ts", "../src/zip-serializer.ts", "../src/crc32.ts", "../src/logger.ts", "../src/compression.ts", "../src/entry-buffer.ts", "../src/parallel-reader.ts", "../src/write-queue.ts", "../src/direct-stream-entry.ts", "../src/streaming-zip-writer.ts", "../src/stream-utils.ts"], "sourcesContent": ["/**\n * Streaming Zipper - A TypeScript library for creating ZIP files in a streaming and parallel way\n * Supports both Web Streams and Node.js streams with ZIP64 support\n */\n\n// Export main classes and interfaces\nexport { StreamingZipWriter, type StreamingZipWriterOptions } from './streaming-zip-writer.js';\nexport { \n type ZipEntry,\n type FastPathStoreEntry,\n type FastPathDeflateEntry,\n isFastPathStoreEntry,\n isFastPathDeflateEntry,\n canUseFastPath,\n UNIX_FILE_TYPES,\n DEFAULT_PERMISSIONS,\n createExternalAttributes\n} from './zip-format.js';\nexport { type CompressionMethod, compressStore, compressDeflate } from './compression.js';\n\n// Export utilities\nexport { CRC32Stream, crc32 } from './crc32.js';\nexport { \n nodeStreamToWebStream, \n uint8ArrayToStream, \n streamToUint8Array,\n createPassThroughStream,\n createByteCounterStream\n} from './stream-utils.js';\n\n// Export constants\nexport { ZIP_CONSTANTS } from './zip-format.js';\n\n// Re-export StreamingZipWriter as default for convenience\nexport { StreamingZipWriter as default } from './streaming-zip-writer.js';", "/**\n * ZIP file format structures and constants\n * Based on ZIP specification: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT\n */\n\nexport const ZIP_CONSTANTS = {\n // Signatures\n LOCAL_FILE_HEADER_SIGNATURE: 0x04034b50,\n CENTRAL_DIRECTORY_SIGNATURE: 0x02014b50,\n END_OF_CENTRAL_DIRECTORY_SIGNATURE: 0x06054b50,\n ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE: 0x06064b50,\n ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE: 0x07064b50,\n DATA_DESCRIPTOR_SIGNATURE: 0x08074b50,\n\n // Compression methods\n COMPRESSION_STORE: 0,\n COMPRESSION_DEFLATE: 8,\n\n // General purpose bit flags\n FLAG_ENCRYPTED: 0x0001,\n FLAG_DATA_DESCRIPTOR: 0x0008,\n FLAG_UTF8: 0x0800,\n\n // ZIP64 constants\n ZIP64_LIMIT: 0xffffffff,\n ZIP64_LIMIT_16: 0xffff,\n ZIP64_EXTRA_FIELD_TYPE: 0x0001,\n\n // Version numbers\n VERSION_MADE_BY: 0x033f, // 3.1 Unix\n VERSION_NEEDED_EXTRACT: 20, // 2.0\n VERSION_NEEDED_EXTRACT_ZIP64: 45, // 4.5\n} as const;\n\nexport interface ZipEntry {\n name: string;\n data: ReadableStream<Uint8Array> | NodeJS.ReadableStream;\n size?: number;\n lastModified?: Date;\n comment?: string;\n permissions?: number; // Unix file permissions (e.g., 0644, 0755)\n \n // Optional metadata for immediate streaming optimization\n crc32?: number; // Pre-calculated CRC32 for immediate streaming\n compressedSize?: number; // For pre-compressed data (required if preCompressed=true)\n uncompressedSize?: number; // For pre-compressed data (required if preCompressed=true)\n preCompressed?: boolean; // Indicates data is already DEFLATE-compressed\n}\n\n/**\n * Fast-path entry for STORE compression with pre-calculated CRC32\n * Enables immediate streaming without buffering\n */\nexport interface FastPathStoreEntry extends ZipEntry {\n crc32: number; // Required for immediate streaming\n size: number; // Required (same as uncompressedSize)\n preCompressed?: false; // Must be false or undefined for STORE\n}\n\n/**\n * Fast-path entry for pre-compressed DEFLATE data\n * Enables immediate streaming of already-compressed data\n */\nexport interface FastPathDeflateEntry extends ZipEntry {\n crc32: number; // Required\n compressedSize: number; // Required\n uncompressedSize: number; // Required \n preCompressed: true; // Must be true for pre-compressed data\n}\n\n/**\n * Type guard to check if entry is a fast-path STORE entry\n */\nexport function isFastPathStoreEntry(entry: ZipEntry): entry is FastPathStoreEntry {\n return (\n typeof entry.crc32 === 'number' &&\n typeof entry.size === 'number' &&\n (entry.preCompressed === undefined || entry.preCompressed === false)\n );\n}\n\n/**\n * Type guard to check if entry is a fast-path DEFLATE entry\n */\nexport function isFastPathDeflateEntry(entry: ZipEntry): entry is FastPathDeflateEntry {\n return (\n typeof entry.crc32 === 'number' &&\n typeof entry.compressedSize === 'number' &&\n typeof entry.uncompressedSize === 'number' &&\n entry.preCompressed === true\n );\n}\n\n/**\n * Check if entry can use fast-path immediate streaming\n */\nexport function canUseFastPath(entry: ZipEntry, compressionMethod: 'store' | 'deflate'): boolean {\n if (compressionMethod === 'store') {\n // For STORE compression, only allow fast-path STORE entries\n return isFastPathStoreEntry(entry);\n } else if (compressionMethod === 'deflate') {\n // For DEFLATE compression, only allow pre-compressed DEFLATE entries\n // STORE entries need compression so they can't use fast-path with DEFLATE writer\n return isFastPathDeflateEntry(entry);\n }\n return false;\n}\n\nexport interface LocalFileHeader {\n signature: number;\n versionNeeded: number;\n flags: number;\n compressionMethod: number;\n lastModTime: number;\n lastModDate: number;\n crc32: number;\n compressedSize: number;\n uncompressedSize: number;\n filenameLength: number;\n extraFieldLength: number;\n filename: Uint8Array;\n extraField: Uint8Array;\n}\n\nexport interface CentralDirectoryHeader {\n signature: number;\n versionMadeBy: number;\n versionNeeded: number;\n flags: number;\n compressionMethod: number;\n lastModTime: number;\n lastModDate: number;\n crc32: number;\n compressedSize: number;\n uncompressedSize: number;\n filenameLength: number;\n extraFieldLength: number;\n commentLength: number;\n diskNumber: number;\n internalAttributes: number;\n externalAttributes: number;\n localHeaderOffset: number;\n filename: Uint8Array;\n extraField: Uint8Array;\n comment: Uint8Array;\n}\n\nexport interface EndOfCentralDirectory {\n signature: number;\n diskNumber: number;\n centralDirDisk: number;\n centralDirRecords: number;\n totalRecords: number;\n centralDirSize: number;\n centralDirOffset: number;\n commentLength: number;\n comment: Uint8Array;\n}\n\nexport interface Zip64EndOfCentralDirectory {\n signature: number;\n recordSize: bigint;\n versionMadeBy: number;\n versionNeeded: number;\n diskNumber: number;\n centralDirDisk: number;\n centralDirRecords: bigint;\n totalRecords: bigint;\n centralDirSize: bigint;\n centralDirOffset: bigint;\n}\n\nexport interface Zip64EndOfCentralDirectoryLocator {\n signature: number;\n zip64EndDisk: number;\n zip64EndOffset: bigint;\n totalDisks: number;\n}\n\nexport interface DataDescriptor {\n signature?: number;\n crc32: number;\n compressedSize: number;\n uncompressedSize: number;\n}\n\nexport interface Zip64DataDescriptor {\n signature?: number;\n crc32: number;\n compressedSize: bigint;\n uncompressedSize: bigint;\n}\n\nexport interface Zip64ExtraField {\n type: number;\n size: number;\n uncompressedSize?: bigint;\n compressedSize?: bigint;\n localHeaderOffset?: bigint;\n diskNumber?: number;\n}\n\n/**\n * Converts a Date to DOS date/time format\n */\nexport function dateToDosDateTime(date: Date): { date: number; time: number } {\n const year = date.getFullYear();\n const month = date.getMonth() + 1;\n const day = date.getDate();\n const hours = date.getHours();\n const minutes = date.getMinutes();\n const seconds = Math.floor(date.getSeconds() / 2);\n\n const dosDate = ((year - 1980) << 9) | (month << 5) | day;\n const dosTime = (hours << 11) | (minutes << 5) | seconds;\n\n return { date: dosDate, time: dosTime };\n}\n\n/**\n * Determines if ZIP64 format is needed based on sizes\n */\nexport function needsZip64(\n uncompressedSize: number | bigint,\n compressedSize: number | bigint,\n localHeaderOffset: number | bigint,\n centralDirSize: number | bigint,\n entryCount: number\n): boolean {\n return (\n Number(uncompressedSize) >= ZIP_CONSTANTS.ZIP64_LIMIT ||\n Number(compressedSize) >= ZIP_CONSTANTS.ZIP64_LIMIT ||\n Number(localHeaderOffset) >= ZIP_CONSTANTS.ZIP64_LIMIT ||\n Number(centralDirSize) >= ZIP_CONSTANTS.ZIP64_LIMIT ||\n entryCount >= ZIP_CONSTANTS.ZIP64_LIMIT_16\n );\n}\n\n/**\n * Creates a ZIP64 extra field\n */\nexport function createZip64ExtraField(\n uncompressedSize?: bigint,\n compressedSize?: bigint,\n localHeaderOffset?: bigint,\n diskNumber?: number\n): Uint8Array {\n const fields: bigint[] = [];\n \n if (uncompressedSize !== undefined) fields.push(uncompressedSize);\n if (compressedSize !== undefined) fields.push(compressedSize);\n if (localHeaderOffset !== undefined) fields.push(localHeaderOffset);\n if (diskNumber !== undefined) fields.push(BigInt(diskNumber));\n\n const size = fields.length * 8 + (diskNumber !== undefined ? -4 : 0);\n const buffer = new ArrayBuffer(4 + size);\n const view = new DataView(buffer);\n \n view.setUint16(0, ZIP_CONSTANTS.ZIP64_EXTRA_FIELD_TYPE, true);\n view.setUint16(2, size, true);\n \n let offset = 4;\n for (const field of fields) {\n if (field === BigInt(diskNumber!) && diskNumber !== undefined) {\n view.setUint32(offset, Number(field), true);\n offset += 4;\n } else {\n view.setBigUint64(offset, field, true);\n offset += 8;\n }\n }\n \n return new Uint8Array(buffer);\n}\n\n/**\n * Constants for Unix file permissions and types\n */\nexport const UNIX_FILE_TYPES = {\n REGULAR_FILE: 0o100000, // S_IFREG\n DIRECTORY: 0o040000, // S_IFDIR\n SYMBOLIC_LINK: 0o120000, // S_IFLNK\n} as const;\n\nexport const DEFAULT_PERMISSIONS = {\n FILE: 0o644, // rw-r--r--\n DIRECTORY: 0o755, // rwxr-xr-x\n EXECUTABLE: 0o755, // rwxr-xr-x\n} as const;\n\n/**\n * Creates external attributes for ZIP entry with Unix permissions\n */\nexport function createExternalAttributes(permissions?: number, isDirectory?: boolean): number {\n // Default permissions based on file type\n let unixPermissions = permissions;\n if (unixPermissions === undefined) {\n unixPermissions = isDirectory ? DEFAULT_PERMISSIONS.DIRECTORY : DEFAULT_PERMISSIONS.FILE;\n }\n \n // Add file type bits\n const fileType = isDirectory ? UNIX_FILE_TYPES.DIRECTORY : UNIX_FILE_TYPES.REGULAR_FILE;\n const unixMode = fileType | unixPermissions;\n \n // Store Unix mode in high 16 bits, DOS attributes in low 16 bits\n // DOS attributes: 0x10 for directory, 0x20 for regular file (archive bit)\n const dosAttributes = isDirectory ? 0x10 : 0x20;\n \n // Use unsigned right shift to ensure proper unsigned 32-bit result\n return ((unixMode << 16) | dosAttributes) >>> 0;\n}", "/**\n * ZIP binary data serialization utilities\n */\n\nimport {\n LocalFileHeader,\n CentralDirectoryHeader,\n EndOfCentralDirectory,\n Zip64EndOfCentralDirectory,\n Zip64EndOfCentralDirectoryLocator,\n DataDescriptor,\n Zip64DataDescriptor,\n} from './zip-format.js';\n\n/**\n * Serializes a Local File Header to binary data\n */\nexport function serializeLocalFileHeader(header: LocalFileHeader): Uint8Array {\n const totalSize = 30 + header.filenameLength + header.extraFieldLength;\n const buffer = new ArrayBuffer(totalSize);\n const view = new DataView(buffer);\n \n view.setUint32(0, header.signature, true);\n view.setUint16(4, header.versionNeeded, true);\n view.setUint16(6, header.flags, true);\n view.setUint16(8, header.compressionMethod, true);\n view.setUint16(10, header.lastModTime, true);\n view.setUint16(12, header.lastModDate, true);\n view.setUint32(14, header.crc32, true);\n view.setUint32(18, header.compressedSize, true);\n view.setUint32(22, header.uncompressedSize, true);\n view.setUint16(26, header.filenameLength, true);\n view.setUint16(28, header.extraFieldLength, true);\n \n const result = new Uint8Array(buffer);\n result.set(header.filename, 30);\n result.set(header.extraField, 30 + header.filenameLength);\n \n return result;\n}\n\n/**\n * Serializes a Central Directory Header to binary data\n */\nexport function serializeCentralDirectoryHeader(header: CentralDirectoryHeader): Uint8Array {\n const totalSize = 46 + header.filenameLength + header.extraFieldLength + header.commentLength;\n const buffer = new ArrayBuffer(totalSize);\n const view = new DataView(buffer);\n \n view.setUint32(0, header.signature, true);\n view.setUint16(4, header.versionMadeBy, true);\n view.setUint16(6, header.versionNeeded, true);\n view.setUint16(8, header.flags, true);\n view.setUint16(10, header.compressionMethod, true);\n view.setUint16(12, header.lastModTime, true);\n view.setUint16(14, header.lastModDate, true);\n view.setUint32(16, header.crc32, true);\n view.setUint32(20, header.compressedSize, true);\n view.setUint32(24, header.uncompressedSize, true);\n view.setUint16(28, header.filenameLength, true);\n view.setUint16(30, header.extraFieldLength, true);\n view.setUint16(32, header.commentLength, true);\n view.setUint16(34, header.diskNumber, true);\n view.setUint16(36, header.internalAttributes, true);\n view.setUint32(38, header.externalAttributes, true);\n view.setUint32(42, header.localHeaderOffset, true);\n \n const result = new Uint8Array(buffer);\n let offset = 46;\n result.set(header.filename, offset);\n offset += header.filenameLength;\n result.set(header.extraField, offset);\n offset += header.extraFieldLength;\n result.set(header.comment, offset);\n \n return result;\n}\n\n/**\n * Serializes an End of Central Directory record to binary data\n */\nexport function serializeEndOfCentralDirectory(eocd: EndOfCentralDirectory): Uint8Array {\n const totalSize = 22 + eocd.commentLength;\n const buffer = new ArrayBuffer(totalSize);\n const view = new DataView(buffer);\n \n view.setUint32(0, eocd.signature, true);\n view.setUint16(4, eocd.diskNumber, true);\n view.setUint16(6, eocd.centralDirDisk, true);\n view.setUint16(8, eocd.centralDirRecords, true);\n view.setUint16(10, eocd.totalRecords, true);\n view.setUint32(12, eocd.centralDirSize, true);\n view.setUint32(16, eocd.centralDirOffset, true);\n view.setUint16(20, eocd.commentLength, true);\n \n const result = new Uint8Array(buffer);\n result.set(eocd.comment, 22);\n \n return result;\n}\n\n/**\n * Serializes a ZIP64 End of Central Directory record to binary data\n */\nexport function serializeZip64EndOfCentralDirectory(eocd: Zip64EndOfCentralDirectory): Uint8Array {\n const buffer = new ArrayBuffer(56);\n const view = new DataView(buffer);\n \n view.setUint32(0, eocd.signature, true);\n view.setBigUint64(4, eocd.recordSize, true);\n view.setUint16(12, eocd.versionMadeBy, true);\n view.setUint16(14, eocd.versionNeeded, true);\n view.setUint32(16, eocd.diskNumber, true);\n view.setUint32(20, eocd.centralDirDisk, true);\n view.setBigUint64(24, eocd.centralDirRecords, true);\n view.setBigUint64(32, eocd.totalRecords, true);\n view.setBigUint64(40, eocd.centralDirSize, true);\n view.setBigUint64(48, eocd.centralDirOffset, true);\n \n return new Uint8Array(buffer);\n}\n\n/**\n * Serializes a ZIP64 End of Central Directory Locator to binary data\n */\nexport function serializeZip64EndOfCentralDirectoryLocator(locator: Zip64EndOfCentralDirectoryLocator): Uint8Array {\n const buffer = new ArrayBuffer(20);\n const view = new DataView(buffer);\n \n view.setUint32(0, locator.signature, true);\n view.setUint32(4, locator.zip64EndDisk, true);\n view.setBigUint64(8, locator.zip64EndOffset, true);\n view.setUint32(16, locator.totalDisks, true);\n \n return new Uint8Array(buffer);\n}\n\n/**\n * Serializes a Data Descriptor to binary data\n */\nexport function serializeDataDescriptor(descriptor: DataDescriptor): Uint8Array {\n const hasSignature = descriptor.signature !== undefined;\n const size = hasSignature ? 16 : 12;\n const buffer = new ArrayBuffer(size);\n const view = new DataView(buffer);\n \n let offset = 0;\n if (hasSignature) {\n view.setUint32(offset, descriptor.signature!, true);\n offset += 4;\n }\n \n view.setUint32(offset, descriptor.crc32, true);\n view.setUint32(offset + 4, descriptor.compressedSize, true);\n view.setUint32(offset + 8, descriptor.uncompressedSize, true);\n \n return new Uint8Array(buffer);\n}\n\n/**\n * Serializes a ZIP64 Data Descriptor to binary data\n */\nexport function serializeZip64DataDescriptor(descriptor: Zip64DataDescriptor): Uint8Array {\n const hasSignature = descriptor.signature !== undefined;\n const size = hasSignature ? 24 : 20;\n const buffer = new ArrayBuffer(size);\n const view = new DataView(buffer);\n \n let offset = 0;\n if (hasSignature) {\n view.setUint32(offset, descriptor.signature!, true);\n offset += 4;\n }\n \n view.setUint32(offset, descriptor.crc32, true);\n view.setBigUint64(offset + 4, descriptor.compressedSize, true);\n view.setBigUint64(offset + 12, descriptor.uncompressedSize, true);\n \n return new Uint8Array(buffer);\n}", "/**\n * Fast CRC32 calculation for ZIP file integrity\n * Based on the CRC32 algorithm used in ZIP files (IEEE 802.3)\n */\n\nlet crcTable: Uint32Array | null = null;\n\n/**\n * Generates the CRC32 lookup table (lazy initialization)\n */\nfunction generateCrcTable(): Uint32Array {\n if (crcTable) return crcTable;\n \n crcTable = new Uint32Array(256);\n \n for (let i = 0; i < 256; i++) {\n let crc = i;\n for (let j = 0; j < 8; j++) {\n if (crc & 1) {\n crc = (crc >>> 1) ^ 0xedb88320;\n } else {\n crc = crc >>> 1;\n }\n }\n crcTable[i] = crc;\n }\n \n return crcTable;\n}\n\n/**\n * Calculate CRC32 for a Uint8Array\n */\nexport function crc32(data: Uint8Array, crc = 0): number {\n const table = generateCrcTable();\n crc = crc ^ 0xffffffff;\n \n for (let i = 0; i < data.length; i++) {\n crc = table[(crc ^ data[i]!) & 0xff]! ^ (crc >>> 8);\n }\n \n return (crc ^ 0xffffffff) >>> 0;\n}\n\n/**\n * CRC32 streaming calculator for processing data in chunks\n */\nexport class CRC32Stream {\n private crc = 0;\n private table: Uint32Array;\n \n constructor() {\n this.table = generateCrcTable();\n this.crc = 0xffffffff;\n }\n \n /**\n * Update CRC32 with new data chunk\n */\n update(data: Uint8Array): void {\n for (let i = 0; i < data.length; i++) {\n this.crc = this.table[(this.crc ^ data[i]!) & 0xff]! ^ (this.crc >>> 8);\n }\n }\n \n /**\n * Get the final CRC32 value\n */\n digest(): number {\n return (this.crc ^ 0xffffffff) >>> 0;\n }\n \n /**\n * Reset the CRC32 calculator\n */\n reset(): void {\n this.crc = 0xffffffff;\n }\n \n /**\n * Get current CRC32 value without finalization\n */\n getCurrentValue(): number {\n return (this.crc ^ 0xffffffff) >>> 0;\n }\n}", "/**\n * Logger configuration using isomorphic-logger\n * Works in both Node.js and browser environments\n */\n\nimport { createLogger } from '@lyleunderwood/isomorphic-logger';\n\n// Create logger instance\nexport const logger = createLogger({\n level: process.env.LOG_LEVEL as any || 'info',\n});\n\n// Export logger for use throughout the library\nexport default logger;\n", "/**\n * Compression handling for ZIP entries\n * Supports both STORE (no compression) and DEFLATE compression\n */\n\nimport { CRC32Stream } from './crc32.js';\nimport { ZIP_CONSTANTS } from './zip-format.js';\nimport { logger } from './logger.js';\n\nexport interface CompressionResult {\n compressedData: Uint8Array;\n crc32: number;\n compressedSize: number;\n uncompressedSize: number;\n}\n\nexport interface CompressionStreamResult {\n readable: ReadableStream<Uint8Array>;\n crc32Promise: Promise<number>;\n compressedSizePromise: Promise<number>;\n uncompressedSizePromise: Promise<number>;\n}\n\n/**\n * Compression method enum\n */\nexport type CompressionMethod = 'store' | 'deflate';\n\n/**\n * Compress data using the STORE method (no compression)\n */\nexport async function compressStore(data: Uint8Array): Promise<CompressionResult> {\n const crc32 = new CRC32Stream();\n crc32.update(data);\n\n return {\n compressedData: data,\n crc32: crc32.digest(),\n compressedSize: data.length,\n uncompressedSize: data.length,\n };\n}\n\n/**\n * Compress data using DEFLATE\n */\nexport async function compressDeflate(data: Uint8Array): Promise<CompressionResult> {\n const crc32Stream = new CRC32Stream();\n crc32Stream.update(data);\n\n // Use built-in compression if available (Node.js or browser)\n try {\n // Try Node.js zlib first\n const zlib = await import('zlib').catch(() => null);\n if (zlib) {\n const compressed = zlib.deflateRawSync(data);\n return {\n compressedData: new Uint8Array(compressed),\n crc32: crc32Stream.digest(),\n compressedSize: compressed.length,\n uncompressedSize: data.length,\n };\n }\n\n // Try browser CompressionStream\n if (typeof CompressionStream !== 'undefined') {\n const stream = new CompressionStream('deflate-raw');\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n\n writer.write(data);\n writer.close();\n\n const chunks: Uint8Array[] = [];\n let result;\n while (!(result = await reader.read()).done) {\n chunks.push(result.value);\n }\n\n const compressed = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));\n let offset = 0;\n for (const chunk of chunks) {\n compressed.set(chunk, offset);\n offset += chunk.length;\n }\n\n return {\n compressedData: compressed,\n crc32: crc32Stream.digest(),\n compressedSize: compressed.length,\n uncompressedSize: data.length,\n };\n }\n } catch (error) {\n // Fall back to store method if compression fails\n // @ts-ignore\n logger.warn({ error: error.message, stack: error.stack }, 'Deflate compression failed, falling back to store method');\n }\n\n // Fallback to store method\n return compressStore(data);\n}\n\n/**\n * Create a compression stream for streaming data\n */\nexport function createCompressionStream(\n inputStream: ReadableStream<Uint8Array> | NodeJS.ReadableStream,\n method: CompressionMethod = 'deflate'\n): CompressionStreamResult {\n const crc32Stream = new CRC32Stream();\n let compressedSize = 0;\n let uncompressedSize = 0;\n let crc32Resolver!: (value: number) => void;\n let compressedSizeResolver!: (value: number) => void;\n let uncompressedSizeResolver!: (value: number) => void;\n\n const crc32Promise = new Promise<number>((resolve) => {\n crc32Resolver = resolve;\n });\n\n const compressedSizePromise = new Promise<number>((resolve) => {\n compressedSizeResolver = resolve;\n });\n\n const uncompressedSizePromise = new Promise<number>((resolve) => {\n uncompressedSizeResolver = resolve;\n });\n\n // Convert Node.js stream to Web Stream if needed\n const webStream = 'getReader' in inputStream\n ? inputStream\n : new ReadableStream<Uint8Array>({\n start(controller) {\n const nodeStream = inputStream as NodeJS.ReadableStream;\n nodeStream.on('data', (chunk: Buffer) => {\n const uint8Array = new Uint8Array(chunk);\n controller.enqueue(uint8Array);\n });\n nodeStream.on('end', () => controller.close());\n nodeStream.on('error', (err) => controller.error(err));\n }\n });\n\n if (method === 'store') {\n // Store method - no compression\n const storeTransform = new TransformStream({\n transform(chunk, controller) {\n crc32Stream.update(chunk);\n uncompressedSize += chunk.length;\n compressedSize += chunk.length;\n controller.enqueue(chunk);\n },\n flush() {\n crc32Resolver(crc32Stream.digest());\n compressedSizeResolver(compressedSize);\n uncompressedSizeResolver(uncompressedSize);\n }\n });\n\n const readable = webStream.pipeThrough(storeTransform);\n\n return {\n readable,\n crc32Promise,\n compressedSizePromise,\n uncompressedSizePromise,\n };\n } else {\n // DEFLATE method - actual compression\n return createDeflateCompressionStream(\n webStream,\n crc32Stream,\n crc32Resolver,\n compressedSizeResolver,\n uncompressedSizeResolver,\n crc32Promise,\n compressedSizePromise,\n uncompressedSizePromise\n );\n }\n}\n\n/**\n * Create DEFLATE compression stream with proper handling\n */\nfunction createDeflateCompressionStream(\n webStream: ReadableStream<Uint8Array>,\n crc32Stream: CRC32Stream,\n crc32Resolver: (value: number) => void,\n compressedSizeResolver: (value: number) => void,\n uncompressedSizeResolver: (value: number) => void,\n crc32Promise: Promise<number>,\n compressedSizePromise: Promise<number>,\n uncompressedSizePromise: Promise<number>\n): CompressionStreamResult {\n let compressedSize = 0;\n let uncompressedSize = 0;\n\n // Try Node.js zlib first\n if (typeof process !== 'undefined' && process.versions && process.versions.node) {\n try {\n // Create a transform that calculates CRC32 on uncompressed data\n const crcTransform = new TransformStream({\n transform(chunk, controller) {\n crc32Stream.update(chunk);\n uncompressedSize += chunk.length;\n controller.enqueue(chunk);\n }\n });\n\n // Create a transform that counts compressed bytes\n const sizeCounterTransform = new TransformStream({\n transform(chunk, controller) {\n compressedSize += chunk.length;\n controller.enqueue(chunk);\n },\n flush() {\n crc32Resolver(crc32Stream.digest());\n compressedSizeResolver(compressedSize);\n uncompressedSizeResolver(uncompressedSize);\n }\n });\n\n // Create Node.js deflate stream wrapped as Web Stream\n const deflateStream = createNodeDeflateStream();\n\n const readable = webStream\n .pipeThrough(crcTransform)\n .pipeThrough(deflateStream)\n .pipeThrough(sizeCounterTransform);\n\n return {\n readable,\n crc32Promise,\n compressedSizePromise,\n uncompressedSizePromise,\n };\n } catch (error) {\n // @ts-ignore\n logger.warn({ error: error.message, stack: error.stack }, 'Node.js deflate failed, falling back to browser compression');\n }\n }\n\n // Try browser CompressionStream\n if (typeof CompressionStream !== 'undefined') {\n try {\n const crcTransform = new TransformStream({\n transform(chunk, controller) {\n crc32Stream.update(chunk);\n uncompressedSize += chunk.length;\n controller.enqueue(chunk);\n }\n });\n\n const sizeCounterTransform = new TransformStream({\n transform(chunk, controller) {\n compressedSize += chunk.length;\n controller.enqueue(chunk);\n },\n flush() {\n crc32Resolver(crc32Stream.digest());\n compressedSizeResolver(compressedSize);\n uncompressedSizeResolver(uncompressedSize);\n }\n });\n\n const deflateStream = new CompressionStream('deflate-raw');\n\n const readable = webStream\n .pipeThrough(crcTransform)\n .pipeThrough(deflateStream)\n .pipeThrough(sizeCounterTransform);\n\n return {\n readable,\n crc32Promise,\n compressedSizePromise,\n uncompressedSizePromise,\n };\n } catch (error) {\n // @ts-ignore\n logger.warn({ error: error.message, stack: error.stack }, 'Browser compression failed, falling back to store method');\n }\n }\n\n // Fallback to store method if compression is not available\n logger.warn('No compression available, using store method');\n return createCompressionStream(webStream, 'store');\n}\n\n/**\n * Create Node.js deflate stream wrapped as Web Stream\n */\nfunction createNodeDeflateStream(): TransformStream<Uint8Array, Uint8Array> {\n let deflate: any = null;\n let streamClosed = false;\n\n return new TransformStream({\n async start(controller) {\n try {\n const zlib = await import('zlib');\n deflate = zlib.createDeflateRaw();\n\n deflate.on('data', (chunk: Buffer) => {\n if (!streamClosed) {\n controller.enqueue(new Uint8Array(chunk));\n }\n });\n\n deflate.on('end', () => {\n if (!streamClosed) {\n streamClosed = true;\n controller.terminate();\n }\n });\n\n deflate.on('error', (err: Error) => {\n if (!streamClosed) {\n streamClosed = true;\n controller.error(err);\n }\n });\n } catch (error) {\n controller.error(error);\n }\n },\n\n transform(chunk, controller) {\n if (deflate && !streamClosed) {\n deflate.write(Buffer.from(chunk));\n } else if (!deflate) {\n controller.error(new Error('Deflate stream not initialized'));\n }\n },\n\n flush(controller) {\n return new Promise<void>((resolve, reject) => {\n if (!deflate || streamClosed) {\n resolve();\n return;\n }\n\n const onEnd = () => {\n deflate.removeListener('error', onError);\n if (!streamClosed) {\n streamClosed = true;\n controller.terminate();\n }\n resolve();\n };\n\n const onError = (err: Error) => {\n deflate.removeListener('end', onEnd);\n if (!streamClosed) {\n streamClosed = true;\n controller.error(err);\n }\n reject(err);\n };\n\n deflate.once('end', onEnd);\n deflate.once('error', onError);\n deflate.end();\n });\n }\n });\n}\n\n/**\n * Get compression method constant\n */\nexport function getCompressionMethodConstant(method: CompressionMethod): number {\n switch (method) {\n case 'store':\n return ZIP_CONSTANTS.COMPRESSION_STORE;\n case 'deflate':\n return ZIP_CONSTANTS.COMPRESSION_DEFLATE;\n default:\n return ZIP_CONSTANTS.COMPRESSION_STORE;\n }\n}\n", "/**\n * Entry buffer system for parallel reading with sequential writing\n */\n\nimport { CRC32Stream } from './crc32.js';\nimport { ZipEntry } from './zip-format.js';\nimport { createCompressionStream, CompressionMethod } from './compression.js';\nimport logger from './logger.js';\n\nexport enum EntryState {\n PENDING = 'pending',\n READING = 'reading',\n READY = 'ready',\n WRITING = 'writing',\n COMPLETED = 'completed',\n ERROR = 'error'\n}\n\nexport interface BufferedChunk {\n data: Uint8Array;\n offset: number;\n}\n\nexport interface EntryMetadata {\n crc32: number;\n compressedSize: number;\n uncompressedSize: number;\n localHeaderOffset: number;\n compressionMethod: CompressionMethod;\n}\n\nexport class EntryBuffer {\n private chunks: BufferedChunk[] = [];\n private totalSize = 0;\n private maxBufferSize: number;\n private crc32Stream = new CRC32Stream();\n private state: EntryState = EntryState.PENDING;\n private readPromise: Promise<void> | null = null;\n private readResolver: (() => void) | null = null;\n private readError: Error | null = null;\n private metadata: Partial<EntryMetadata> = {};\n \n // Backpressure coordination\n private waitingReaders: Array<() => void> = [];\n\n constructor(\n public readonly entry: ZipEntry,\n public readonly index: number,\n public readonly compressionMethod: CompressionMethod = 'store',\n maxBufferSize = 1024 * 1024 // 1MB default buffer limit\n ) {\n this.maxBufferSize = maxBufferSize;\n }\n\n /**\n * Get current entry state\n */\n getState(): EntryState {\n return this.state;\n }\n\n /**\n * Check if entry is ready to be written\n */\n isReady(): boolean {\n return this.state === EntryState.READY;\n }\n\n /**\n * Check if entry has been completely read\n */\n isReadComplete(): boolean {\n return this.state === EntryState.READY || this.state === EntryState.WRITING || this.state === EntryState.COMPLETED || this.state === EntryState.ERROR;\n }\n\n /**\n * Get buffered size in bytes\n */\n getBufferedSize(): number {\n // Calculate actual buffered size from chunks array for accuracy\n const actualSize = this.chunks.reduce((total, chunk) => total + chunk.data.length, 0);\n \n // Verify totalSize is accurate and fix if needed\n if (actualSize !== this.totalSize) {\n logger.debug({ actualSize, totalSize: this.totalSize }, 'Buffer size mismatch detected, correcting');\n this.totalSize = actualSize;\n }\n \n return this.totalSize;\n }\n\n /**\n * Check if buffer has space for more data\n */\n hasBufferSpace(): boolean {\n return this.totalSize < this.maxBufferSize;\n }\n\n /**\n * Start reading from the entry's data stream\n */\n async startReading(): Promise<void> {\n if (this.state !== EntryState.PENDING) {\n throw new Error(`Cannot start reading entry in state: ${this.state}`);\n }\n\n this.state = EntryState.READING;\n this.readPromise = this.performRead();\n return this.readPromise;\n }\n\n /**\n * Wait for entry to be ready for writing\n */\n async waitForReady(): Promise<void> {\n if (this.state === EntryState.READY) {\n return;\n }\n\n if (this.state === EntryState.ERROR) {\n throw this.readError || new Error('Entry reading failed');\n }\n\n if (this.readPromise) {\n await this.readPromise;\n }\n\n // Check state again after waiting - use type assertion to work around TypeScript flow analysis\n const currentState = this.state as EntryState;\n switch (currentState) {\n case EntryState.ERROR:\n throw this.readError || new Error('Entry reading failed');\n case EntryState.READY:\n return;\n default:\n throw new Error(`Unexpected state after reading: ${currentState}`);\n }\n }\n\n /**\n * Read the next chunk from the buffer\n */\n readChunk(): BufferedChunk | null {\n if (this.chunks.length === 0) {\n return null;\n }\n\n // Remove and return the first chunk (FIFO)\n const chunk = this.chunks.shift();\n \n // Update total size to reflect consumed data\n if (chunk) {\n this.totalSize -= chunk.data.length;\n }\n \n return chunk || null;\n }\n\n /**\n * Get entry metadata\n */\n getMetadata(): EntryMetadata {\n if (!this.isReadComplete()) {\n throw new Error('Metadata not available until reading is complete');\n }\n\n return {\n crc32: this.metadata.crc32!,\n compressedSize: this.metadata.compressedSize!,\n uncompressedSize: this.metadata.uncompressedSize!,\n localHeaderOffset: this.metadata.localHeaderOffset || 0,\n compressionMethod: this.compressionMethod\n };\n }\n\n /**\n * Set the local header offset\n */\n setLocalHeaderOffset(offset: number): void {\n this.metadata.localHeaderOffset = offset;\n }\n\n /**\n * Mark entry as being written\n */\n startWriting(): void {\n if (this.state !== EntryState.READY) {\n throw new Error(`Cannot start writing entry in state: ${this.state}`);\n }\n this.state = EntryState.WRITING;\n }\n\n /**\n * Mark entry as completed\n */\n markCompleted(): void {\n if (this.state !== EntryState.WRITING) {\n throw new Error(`Cannot mark entry completed from state: ${this.state}`);\n }\n this.state = EntryState.COMPLETED;\n }\n\n /**\n * Get total uncompressed size\n */\n getUncompressedSize(): number {\n return this.metadata.uncompressedSize || 0;\n }\n\n /**\n * Get total compressed size (same as uncompressed for store method)\n */\n getCompressedSize(): number {\n return this.metadata.compressedSize || 0;\n }\n\n /**\n * Get CRC32 checksum\n */\n getCRC32(): number {\n return this.metadata.crc32 || 0;\n }\n\n /**\n * Perform the actual reading from the stream\n */\n private async performRead(): Promise<void> {\n logger.debug({ entryName: this.entry.name }, 'Starting to read entry data');\n try {\n logger.debug({ entryName: this.entry.name }, 'Reading entry data');\n // Convert Node.js stream to Web Stream if needed\n const inputStream = 'getReader' in this.entry.data\n ? this.entry.data\n : this.createWebStreamFromNodeStream(this.entry.data);\n\n logger.debug('Starting to read entry chunks');\n\n // Create compression stream\n const compressionResult = createCompressionStream(inputStream, this.compressionMethod);\n\n const reader = compressionResult.readable.getReader();\n logger.debug('Entry chunk reader created');\n let chunkOffset = 0;\n\n try {\n while (true) {\n logger.debug('Reading entry chunk');\n const { done, value } = await reader.read();\n logger.debug({ done }, 'Entry chunk read');\n if (done) break;\n\n // Wait for buffer space if needed\n if (!this.hasBufferSpace()) {\n logger.debug('Waiting for buffer space');\n await this.waitForBufferSpace();\n logger.debug('Buffer space available');\n }\n\n // Store compressed chunk\n this.chunks.push({\n data: value,\n offset: chunkOffset\n });\n\n this.totalSize += value.length;\n chunkOffset += value.length;\n }\n } finally {\n logger.debug('Releasing entry chunk reader');\n reader.releaseLock();\n }\n\n // Wait for compression metadata\n const [crc32, compressedSize, uncompressedSize] = await Promise.all([\n compressionResult.crc32Promise,\n compressionResult.compressedSizePromise,\n compressionResult.uncompressedSizePromise\n ]);\n\n // Set final metadata\n this.metadata.crc32 = crc32;\n this.metadata.compressedSize = compressedSize;\n this.metadata.uncompressedSize = uncompressedSize;\n\n this.state = EntryState.READY;\n\n logger.debug({ crc32, compressedSize, uncompressedSize }, 'Entry read complete');\n\n if (this.readResolver) {\n logger.debug('Calling read resolver');\n this.readResolver();\n }\n\n } catch (error) {\n this.readError = error instanceof Error ? error : new Error(String(error));\n this.state = EntryState.ERROR;\n\n if (this.readResolver) {\n this.readResolver();\n }\n }\n }\n\n /**\n * Wait for buffer space to become available\n */\n private async waitForBufferSpace(): Promise<void> {\n // If space is already available, return immediately\n if (this.hasBufferSpace()) {\n return;\n }\n\n // Create a promise that will be resolved when space becomes available\n return new Promise<void>((resolve) => {\n // Double-check space availability after adding to queue to avoid race condition\n if (this.hasBufferSpace()) {\n resolve();\n return;\n }\n \n this.waitingReaders.push(resolve);\n });\n }\n\n /**\n * Notify that buffer space has been freed up (called when chunks are consumed)\n */\n notifySpaceFreed(): void {\n // Always check and resolve waiting readers if space is available\n if (this.waitingReaders.length > 0) {\n // Get current buffer state\n const hasSpace = this.hasBufferSpace();\n \n if (hasSpace) {\n // Resolve all waiting readers\n const resolvers = this.waitingReaders.splice(0);\n logger.debug({ \n bufferedSize: this.getBufferedSize(), \n maxSize: this.maxBufferSize,\n waitingReaders: resolvers.length \n }, 'Notifying waiting readers of available buffer space');\n \n resolvers.forEach(resolve => {\n try {\n resolve();\n } catch (error) {\n logger.error({ error }, 'Error resolving waiting reader');\n }\n });\n }\n }\n }\n\n /**\n * Create a Web ReadableStream from a Node.js stream\n */\n private createWebStreamFromNodeStream(nodeStream: NodeJS.ReadableStream): ReadableStream<Uint8Array> {\n logger.debug('Converting Node.js stream to Web Stream');\n return new ReadableStream<Uint8Array>({\n start(controller) {\n nodeStream.on('data', (chunk: Buffer) => {\n const uint8Array = new Uint8Array(chunk);\n controller.enqueue(uint8Array);\n });\n\n nodeStream.on('end', () => {\n controller.close();\n });\n\n nodeStream.on('error', (err) => {\n controller.error(err);\n });\n },\n\n cancel() {\n if ('destroy' in nodeStream && typeof nodeStream.destroy === 'function') {\n nodeStream.destroy();\n }\n }\n });\n }\n}\n", "/**\n * Parallel reader for concurrent entry stream processing\n */\n\nimport { EntryBuffer, EntryState } from './entry-buffer.js';\nimport { ZipEntry } from './zip-format.js';\nimport { CompressionMethod } from './compression.js';\nimport logger from './logger.js';\n\nexport interface ParallelReaderOptions {\n maxBufferSize?: number;\n maxConcurrentReads?: number;\n compression?: CompressionMethod;\n}\n\nexport class ParallelReader {\n private entryBuffers: EntryBuffer[] = [];\n private readingPromises: Map<number, Promise<void>> = new Map();\n private options: ParallelReaderOptions;\n private activeReads = 0;\n\n constructor(options: ParallelReaderOptions = {}) {\n this.options = {\n maxBufferSize: 1024 * 1024, // 1MB per entry\n maxConcurrentReads: 10, // Max concurrent reads\n compression: 'store', // Default compression\n ...options\n };\n }\n\n /**\n * Add an entry to be read in parallel\n */\n addEntry(entry: ZipEntry): EntryBuffer {\n const index = this.entryBuffers.length;\n const entryBuffer = new EntryBuffer(\n entry,\n index,\n this.options.compression!,\n this.options.maxBufferSize\n );\n\n this.entryBuffers.push(entryBuffer);\n\n // Start reading immediately if we have capacity\n this.startReadingIfPossible(entryBuffer);\n\n return entryBuffer;\n }\n\n /**\n * Get all entry buffers\n */\n getEntryBuffers(): EntryBuffer[] {\n return [...this.entryBuffers];\n }\n\n /**\n * Get entry buffer by index\n */\n getEntryBuffer(index: number): EntryBuffer | undefined {\n return this.entryBuffers[index];\n }\n\n /**\n * Get the next ready entry in order\n */\n getNextReadyEntry(): EntryBuffer | null {\n for (const buffer of this.entryBuffers) {\n if (buffer.getState() === EntryState.READY) {\n return buffer;\n }\n // If we hit a non-ready entry, we can't proceed (order matters)\n if (buffer.getState() !== EntryState.COMPLETED) {\n return null;\n }\n }\n return null;\n }\n\n /**\n * Check if there are any entries still being read\n */\n hasActiveReads(): boolean {\n return this.activeReads > 0 || this.readingPromises.size > 0;\n }\n\n /**\n * Wait for the next entry to become ready\n */\n async waitForNextReady(): Promise<EntryBuffer | null> {\n // Check if next entry is already ready\n const nextReady = this.getNextReadyEntry();\n if (nextReady) {\n return nextReady;\n }\n\n // Find the next entry that needs to be ready\n let nextEntry: EntryBuffer | null = null;\n for (const buffer of this.entryBuffers) {\n if (buffer.getState() === EntryState.COMPLETED) {\n continue;\n }\n nextEntry = buffer;\n break;\n }\n\n if (!nextEntry) {\n return null; // No more entries\n }\n\n // Wait for this entry to be ready\n if (nextEntry.getState() === EntryState.PENDING) {\n this.startReadingIfPossible(nextEntry);\n }\n\n if (nextEntry.getState() !== EntryState.READY) {\n await nextEntry.waitForReady();\n }\n\n return nextEntry.isReady() ? nextEntry : null;\n }\n\n /**\n * Wait for all entries to complete reading\n */\n async waitForAllComplete(): Promise<void> {\n // Start reading any pending entries\n for (const buffer of this.entryBuffers) {\n if (buffer.getState() === EntryState.PENDING) {\n logger.debug({ entryName: buffer.entry.name }, 'Starting to read pending entry');\n this.startReadingIfPossible(buffer);\n }\n }\n\n // Wait for all reading promises to complete\n const promises = Array.from(this.readingPromises.values());\n if (promises.length > 0) {\n logger.debug({ count: promises.length }, 'Waiting for all reading promises to complete');\n await Promise.all(promises);\n }\n }\n\n /**\n * Get statistics about reading progress\n */\n getStats(): {\n total: number;\n pending: number;\n reading: number;\n ready: number;\n writing: number;\n completed: number;\n errors: number;\n } {\n const stats = {\n total: this.entryBuffers.length,\n pending: 0,\n reading: 0,\n ready: 0,\n writing: 0,\n completed: 0,\n errors: 0\n };\n\n for (const buffer of this.entryBuffers) {\n switch (buffer.getState()) {\n case EntryState.PENDING:\n stats.pending++;\n break;\n case EntryState.READING:\n stats.reading++;\n break;\n case EntryState.READY:\n stats.ready++;\n break;\n case EntryState.WRITING:\n stats.writing++;\n break;\n case EntryState.COMPLETED:\n stats.completed++;\n break;\n case EntryState.ERROR:\n stats.errors++;\n break;\n }\n }\n\n return stats;\n }\n\n /**\n * Start reading an entry if we have capacity\n */\n private startReadingIfPossible(entryBuffer: EntryBuffer): void {\n if (entryBuffer.getState() !== EntryState.PENDING) {\n return;\n }\n\n if (this.activeReads >= this.options.maxConcurrentReads!) {\n return; // At capacity\n }\n\n this.activeReads++;\n logger.debug({ entryName: entryBuffer.entry.name }, 'Starting to read entry');\n const readPromise = entryBuffer.startReading().finally(() => {\n logger.debug({ entryName: entryBuffer.entry.name }, 'Completed reading entry');\n this.activeReads--;\n this.readingPromises.delete(entryBuffer.index);\n\n // Try to start reading the next pending entry\n this.startNextPendingEntry();\n });\n\n this.readingPromises.set(entryBuffer.index, readPromise);\n }\n\n /**\n * Try to start reading the next pending entry\n */\n private startNextPendingEntry(): void {\n if (this.activeReads >= this.options.maxConcurrentReads!) {\n return;\n }\n\n for (const buffer of this.entryBuffers) {\n if (buffer.getState() === EntryState.PENDING) {\n this.startReadingIfPossible(buffer);\n break;\n }\n }\n }\n}\n", "/**\n * Write queue for sequential entry writing while maintaining ZIP structure\n */\n\nimport { EntryBuffer, EntryState } from './entry-buffer.js';\nimport { DirectStreamEntry } from './direct-stream-entry.js';\nimport {\n ZIP_CONSTANTS,\n LocalFileHeader,\n CentralDirectoryHeader,\n dateToDosDateTime,\n needsZip64,\n createZip64ExtraField,\n createExternalAttributes\n} from './zip-format.js';\nimport {\n serializeLocalFileHeader,\n serializeCentralDirectoryHeader,\n serializeDataDescriptor,\n serializeZip64DataDescriptor\n} from './zip-serializer.js';\nimport { logger } from './logger.js';\n\nexport interface WriteQueueOptions {\n compression?: 'store' | 'deflate';\n}\n\nexport class WriteQueue {\n private outputController: ReadableStreamDefaultController<Uint8Array> | null = null;\n private currentOffset = 0;\n private centralDirectoryEntries: CentralDirectoryHeader[] = [];\n private options: WriteQueueOptions;\n private writeInProgress = false;\n\n constructor(options: WriteQueueOptions = {}) {\n this.options = {\n compression: 'store',\n ...options\n };\n\n logger.debug({ options: this.options }, 'WriteQueue initialized');\n }\n\n /**\n * Set the output controller for streaming data\n */\n setOutputController(controller: ReadableStreamDefaultController<Uint8Array>): void {\n this.outputController = controller;\n }\n\n /**\n * Write an entry buffer to the output stream\n */\n async writeEntry(entryBuffer: EntryBuffer): Promise<void> {\n if (this.writeInProgress) {\n throw new Error('Another write operation is already in progress');\n }\n\n if (!entryBuffer.isReady()) {\n throw new Error('Entry buffer is not ready for writing');\n }\n\n const entryName = entryBuffer.entry.name;\n logger.debug({ entryName, currentOffset: this.currentOffset }, 'Starting to write buffered entry');\n\n this.writeInProgress = true;\n\n try {\n entryBuffer.startWriting();\n await this.performEntryWrite(entryBuffer);\n entryBuffer.markCompleted();\n logger.debug({ entryName, finalOffset: this.currentOffset }, 'Completed writing buffered entry');\n } catch (error) {\n // @ts-ignore\n logger.error({ entryName, error: error.message, stack: error.stack }, 'Error writing buffered entry');\n throw error;\n } finally {\n this.writeInProgress = false;\n }\n }\n\n /**\n * Write a direct stream entry immediately to the output stream\n */\n async writeDirectStreamEntry(directEntry: DirectStreamEntry): Promise<void> {\