@@ -20,38 +20,6 @@ import {
2020 zstdDecompress
2121} from '../src/index' ;
2222
23- // Helper to collect all chunks from a ReadableStream into a single Uint8Array
24- async function collectStream ( stream : ReadableStream < Uint8Array > ) : Promise < Uint8Array > {
25- const reader = stream . getReader ( ) ;
26- const chunks : Uint8Array [ ] = [ ] ;
27- let totalLength = 0 ;
28-
29- while ( true ) {
30- const { done, value } = await reader . read ( ) ;
31- if ( done ) break ;
32- chunks . push ( value ) ;
33- totalLength += value . length ;
34- }
35-
36- const result = new Uint8Array ( totalLength ) ;
37- let offset = 0 ;
38- for ( const chunk of chunks ) {
39- result . set ( chunk , offset ) ;
40- offset += chunk . length ;
41- }
42- return result ;
43- }
44-
45- // Helper to create a ReadableStream from a Uint8Array
46- function createReadableStream ( data : Uint8Array ) : ReadableStream < Uint8Array > {
47- return new ReadableStream ( {
48- start ( controller ) {
49- controller . enqueue ( data ) ;
50- controller . close ( ) ;
51- }
52- } ) ;
53- }
54-
5523describe ( "Streaming" , ( ) => {
5624 describe ( "Gzip" , ( ) => {
5725 it ( 'should compress data with gzip stream' , async ( ) => {
@@ -132,6 +100,39 @@ describe("Streaming", () => {
132100 const decompressed = zlib . gunzipSync ( Buffer . from ( compressed ) ) ;
133101 expect ( decompressed . toString ( ) ) . to . equal ( repeated ) ;
134102 } ) ;
103+
104+ it ( 'should produce incremental output during compression' , async ( ) => {
105+ const inputChunks = generateLargeData ( STREAMING_TEST_SIZE_KB ) ;
106+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
107+ createGzipStream ( ) ,
108+ inputChunks
109+ ) ;
110+
111+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
112+
113+ const decompressed = zlib . gunzipSync ( Buffer . from ( totalOutput ) ) ;
114+ const expectedSize = inputChunks . reduce ( ( sum , c ) => sum + c . length , 0 ) ;
115+ expect ( decompressed . length ) . to . equal ( expectedSize ) ;
116+ } ) ;
117+
118+ it ( 'should produce incremental output during decompression' , async ( ) => {
119+ const originalData = Buffer . concat ( generateLargeData ( STREAMING_TEST_SIZE_KB ) ) ;
120+ const compressed = zlib . gzipSync ( originalData ) ;
121+
122+ const chunkSize = 16 * 1024 ;
123+ const inputChunks : Uint8Array [ ] = [ ] ;
124+ for ( let i = 0 ; i < compressed . length ; i += chunkSize ) {
125+ inputChunks . push ( compressed . subarray ( i , Math . min ( i + chunkSize , compressed . length ) ) ) ;
126+ }
127+
128+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
129+ createGunzipStream ( ) ,
130+ inputChunks
131+ ) ;
132+
133+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
134+ expect ( totalOutput . length ) . to . equal ( originalData . length ) ;
135+ } ) ;
135136 } ) ;
136137
137138 describe ( "Deflate" , ( ) => {
@@ -168,6 +169,39 @@ describe("Streaming", () => {
168169
169170 expect ( Buffer . from ( result ) . toString ( ) ) . to . equal ( original ) ;
170171 } ) ;
172+
173+ it ( 'should produce incremental output during compression' , async ( ) => {
174+ const inputChunks = generateLargeData ( STREAMING_TEST_SIZE_KB ) ;
175+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
176+ createDeflateStream ( ) ,
177+ inputChunks
178+ ) ;
179+
180+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
181+
182+ const decompressed = zlib . inflateSync ( Buffer . from ( totalOutput ) ) ;
183+ const expectedSize = inputChunks . reduce ( ( sum , c ) => sum + c . length , 0 ) ;
184+ expect ( decompressed . length ) . to . equal ( expectedSize ) ;
185+ } ) ;
186+
187+ it ( 'should produce incremental output during decompression' , async ( ) => {
188+ const originalData = Buffer . concat ( generateLargeData ( STREAMING_TEST_SIZE_KB ) ) ;
189+ const compressed = zlib . deflateSync ( originalData ) ;
190+
191+ const chunkSize = 16 * 1024 ;
192+ const inputChunks : Uint8Array [ ] = [ ] ;
193+ for ( let i = 0 ; i < compressed . length ; i += chunkSize ) {
194+ inputChunks . push ( compressed . subarray ( i , Math . min ( i + chunkSize , compressed . length ) ) ) ;
195+ }
196+
197+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
198+ createInflateStream ( ) ,
199+ inputChunks
200+ ) ;
201+
202+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
203+ expect ( totalOutput . length ) . to . equal ( originalData . length ) ;
204+ } ) ;
171205 } ) ;
172206
173207 describe ( "Deflate-Raw" , ( ) => {
@@ -256,6 +290,49 @@ describe("Streaming", () => {
256290 const decompressed = await brotliDecompress ( compressed ) ;
257291 expect ( Buffer . from ( decompressed ) . toString ( ) ) . to . equal ( repeated ) ;
258292 } ) ;
293+
294+ // Brotli has a larger internal buffer than gzip/deflate, requiring ~1.3MB of
295+ // incompressible data before it produces streaming output. With compressible data
296+ // the output is too small to trigger streaming. We use 2MB of random data here.
297+ it ( 'should produce incremental output during compression' , async function ( ) {
298+ this . timeout ( 10000 ) ; // Can be slow
299+
300+ const BROTLI_DATA_SIZE_KB = 2048 ;
301+ const inputChunks = generateLargeData ( BROTLI_DATA_SIZE_KB , false ) ;
302+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
303+ createBrotliCompressStream ( ) ,
304+ inputChunks
305+ ) ;
306+
307+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
308+
309+ const decompressed = await brotliDecompress ( totalOutput ) ;
310+ const expectedSize = inputChunks . reduce ( ( sum , c ) => sum + c . length , 0 ) ;
311+ expect ( decompressed . length ) . to . equal ( expectedSize ) ;
312+ } ) ;
313+
314+ it ( 'should produce incremental output during decompression' , async function ( ) {
315+ this . timeout ( 10000 ) ; // Can be slow
316+
317+ const BROTLI_DECOMPRESS_SIZE_KB = 1024 ; // 1MB
318+ const originalData = Buffer . concat ( generateLargeData ( BROTLI_DECOMPRESS_SIZE_KB , false ) ) ;
319+ const compressed = await brotliCompress ( originalData ) ;
320+
321+ const chunkSize = 16 * 1024 ;
322+ const inputChunks : Uint8Array [ ] = [ ] ;
323+ for ( let i = 0 ; i < compressed . length ; i += chunkSize ) {
324+ inputChunks . push ( new Uint8Array ( compressed . buffer , compressed . byteOffset + i ,
325+ Math . min ( chunkSize , compressed . length - i ) ) ) ;
326+ }
327+
328+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
329+ createBrotliDecompressStream ( ) ,
330+ inputChunks
331+ ) ;
332+
333+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
334+ expect ( totalOutput . length ) . to . equal ( originalData . length ) ;
335+ } ) ;
259336 } ) ;
260337
261338 describe ( "Zstd" , ( ) => {
@@ -308,5 +385,161 @@ describe("Streaming", () => {
308385 const decompressed = await zstdDecompress ( compressed ) ;
309386 expect ( Buffer . from ( decompressed ) . toString ( ) ) . to . equal ( repeated ) ;
310387 } ) ;
388+
389+ it ( 'should produce incremental output during compression' , async ( ) => {
390+ const inputChunks = generateLargeData ( STREAMING_TEST_SIZE_KB ) ;
391+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
392+ createZstdCompressStream ( ) ,
393+ inputChunks
394+ ) ;
395+
396+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
397+
398+ const decompressed = await zstdDecompress ( totalOutput ) ;
399+ const expectedSize = inputChunks . reduce ( ( sum , c ) => sum + c . length , 0 ) ;
400+ expect ( decompressed . length ) . to . equal ( expectedSize ) ;
401+ } ) ;
402+
403+ it ( 'should produce incremental output during decompression' , async ( ) => {
404+ const originalData = Buffer . concat ( generateLargeData ( STREAMING_TEST_SIZE_KB ) ) ;
405+ const compressed = await zstdCompress ( originalData ) ;
406+
407+ const chunkSize = 16 * 1024 ;
408+ const inputChunks : Uint8Array [ ] = [ ] ;
409+ for ( let i = 0 ; i < compressed . length ; i += chunkSize ) {
410+ inputChunks . push ( new Uint8Array ( compressed . buffer , compressed . byteOffset + i ,
411+ Math . min ( chunkSize , compressed . length - i ) ) ) ;
412+ }
413+
414+ const { outputBeforeEnd, totalOutput } = await testTrueStreaming (
415+ createZstdDecompressStream ( ) ,
416+ inputChunks
417+ ) ;
418+
419+ expect ( outputBeforeEnd ) . to . equal ( true , 'Expected output before input completed' ) ;
420+ expect ( totalOutput . length ) . to . equal ( originalData . length ) ;
421+ } ) ;
311422 } ) ;
312423} ) ;
424+
425+ // Helper to collect all chunks from a ReadableStream into a single Uint8Array
426+ async function collectStream ( stream : ReadableStream < Uint8Array > ) : Promise < Uint8Array > {
427+ const reader = stream . getReader ( ) ;
428+ const chunks : Uint8Array [ ] = [ ] ;
429+ let totalLength = 0 ;
430+
431+ while ( true ) {
432+ const { done, value } = await reader . read ( ) ;
433+ if ( done ) break ;
434+ chunks . push ( value ) ;
435+ totalLength += value . length ;
436+ }
437+
438+ const result = new Uint8Array ( totalLength ) ;
439+ let offset = 0 ;
440+ for ( const chunk of chunks ) {
441+ result . set ( chunk , offset ) ;
442+ offset += chunk . length ;
443+ }
444+ return result ;
445+ }
446+
447+ // Helper to create a ReadableStream from a Uint8Array
448+ function createReadableStream ( data : Uint8Array ) : ReadableStream < Uint8Array > {
449+ return new ReadableStream ( {
450+ start ( controller ) {
451+ controller . enqueue ( data ) ;
452+ controller . close ( ) ;
453+ }
454+ } ) ;
455+ }
456+
457+ // Data size for incremental output tests (256KB is enough for gzip/deflate/zstd)
458+ const STREAMING_TEST_SIZE_KB = 256 ;
459+
460+ // Helper to test true streaming behavior - verifies output arrives before all input is sent
461+ async function testTrueStreaming (
462+ transformStream : TransformStream < Uint8Array , Uint8Array > ,
463+ inputChunks : Uint8Array [ ]
464+ ) : Promise < { outputBeforeEnd : boolean ; totalOutput : Uint8Array } > {
465+ const outputChunks : Uint8Array [ ] = [ ] ;
466+ let outputReceivedBeforeEnd = false ;
467+ let inputComplete = false ;
468+
469+ const writer = transformStream . writable . getWriter ( ) ;
470+ const reader = transformStream . readable . getReader ( ) ;
471+
472+ // Start reading in background - this allows output to be collected as it arrives
473+ const readPromise = ( async ( ) => {
474+ while ( true ) {
475+ const { done, value } = await reader . read ( ) ;
476+ if ( done ) break ;
477+ outputChunks . push ( value ) ;
478+ if ( ! inputComplete ) {
479+ outputReceivedBeforeEnd = true ;
480+ }
481+ }
482+ } ) ( ) ;
483+
484+ // Write chunks with small delays to allow output processing
485+ for ( const chunk of inputChunks ) {
486+ await writer . write ( chunk ) ;
487+ await new Promise ( resolve => setTimeout ( resolve , 0 ) ) ;
488+ }
489+
490+ inputComplete = true ;
491+ await writer . close ( ) ;
492+ await readPromise ;
493+
494+ // Combine output chunks
495+ const totalLength = outputChunks . reduce ( ( sum , chunk ) => sum + chunk . length , 0 ) ;
496+ const totalOutput = new Uint8Array ( totalLength ) ;
497+ let offset = 0 ;
498+ for ( const chunk of outputChunks ) {
499+ totalOutput . set ( chunk , offset ) ;
500+ offset += chunk . length ;
501+ }
502+
503+ return { outputBeforeEnd : outputReceivedBeforeEnd , totalOutput } ;
504+ }
505+
506+ // Mulberry32: high-quality 32-bit seeded PRNG that produces incompressible output
507+ function createSeededRng ( seed : number ) {
508+ return function ( ) : number {
509+ let t = seed += 0x6D2B79F5 ;
510+ t = Math . imul ( t ^ t >>> 15 , t | 1 ) ;
511+ t ^= t + Math . imul ( t ^ t >>> 7 , t | 61 ) ;
512+ return ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296 ;
513+ } ;
514+ }
515+
516+ // Generate large test data that will produce streaming output
517+ function generateLargeData ( sizeKB : number , compressible = true ) : Uint8Array [ ] {
518+ const chunkSize = 16 * 1024 ; // 16KB chunks
519+ const totalSize = sizeKB * 1024 ;
520+ const chunks : Uint8Array [ ] = [ ] ;
521+ const pattern = 'This is test data that will be repeated to create compressible content. ' ;
522+
523+ // Fixed seed for deterministic, reproducible test data
524+ const rng = createSeededRng ( 0x12345678 ) ;
525+
526+ let remaining = totalSize ;
527+ while ( remaining > 0 ) {
528+ const size = Math . min ( chunkSize , remaining ) ;
529+ const chunk = new Uint8Array ( size ) ;
530+ if ( compressible ) {
531+ for ( let i = 0 ; i < size ; i ++ ) {
532+ chunk [ i ] = pattern . charCodeAt ( i % pattern . length ) ;
533+ }
534+ } else {
535+ // Use seeded PRNG for deterministic incompressible data
536+ for ( let i = 0 ; i < size ; i ++ ) {
537+ chunk [ i ] = Math . floor ( rng ( ) * 256 ) ;
538+ }
539+ }
540+ chunks . push ( chunk ) ;
541+ remaining -= size ;
542+ }
543+
544+ return chunks ;
545+ }
0 commit comments