@@ -28,11 +28,13 @@ const kHandle = Symbol('handle');
2828const kFlags = Symbol ( 'flags' ) ;
2929const kEncoding = Symbol ( 'encoding' ) ;
3030const kDecoder = Symbol ( 'decoder' ) ;
31+ const kChunk = Symbol ( 'chunk' ) ;
3132const kFatal = Symbol ( 'kFatal' ) ;
3233const kUTF8FastPath = Symbol ( 'kUTF8FastPath' ) ;
3334const kIgnoreBOM = Symbol ( 'kIgnoreBOM' ) ;
3435
3536const { isSinglebyteEncoding, createSinglebyteDecoder } = require ( 'internal/encoding/single-byte' ) ;
37+ const { unfinishedBytesUtf8, mergePrefixUtf8 } = require ( 'internal/encoding/util' ) ;
3638
3739const {
3840 getConstructorOf,
@@ -447,9 +449,11 @@ class TextDecoder {
447449 this [ kUTF8FastPath ] = false ;
448450 this [ kHandle ] = undefined ;
449451 this [ kSingleByte ] = undefined ; // Does not care about streaming or BOM
452+ this [ kChunk ] = null ; // A copy of previous streaming tail or null
450453
451454 if ( enc === 'utf-8' ) {
452455 this [ kUTF8FastPath ] = true ;
456+ this [ kBOMSeen ] = false ;
453457 } else if ( isSinglebyteEncoding ( enc ) ) {
454458 this [ kSingleByte ] = createSinglebyteDecoder ( enc , this [ kFatal ] ) ;
455459 } else {
@@ -458,15 +462,14 @@ class TextDecoder {
458462 }
459463
460464 #prepareConverter( ) {
461- if ( this [ kHandle ] !== undefined ) return ;
462465 if ( hasIntl ) {
463466 let icuEncoding = this [ kEncoding ] ;
464467 if ( icuEncoding === 'gbk' ) icuEncoding = 'gb18030' ; // 10.1.1. GBK's decoder is gb18030's decoder
465468 const handle = icuGetConverter ( icuEncoding , this [ kFlags ] ) ;
466469 if ( handle === undefined )
467470 throw new ERR_ENCODING_NOT_SUPPORTED ( this [ kEncoding ] ) ;
468471 this [ kHandle ] = handle ;
469- } else if ( this [ kEncoding ] === 'utf-8' || this [ kEncoding ] === 'utf- 16le') {
472+ } else if ( this [ kEncoding ] === 'utf-16le' ) {
470473 if ( this [ kFatal ] ) throw new ERR_NO_ICU ( '"fatal" option' ) ;
471474 this [ kHandle ] = new ( lazyStringDecoder ( ) ) ( this [ kEncoding ] ) ;
472475 this [ kBOMSeen ] = false ;
@@ -483,11 +486,55 @@ class TextDecoder {
483486
484487 const stream = options ?. stream ;
485488 if ( this [ kUTF8FastPath ] ) {
486- if ( ! stream ) return decodeUTF8 ( input , this [ kIgnoreBOM ] , this [ kFatal ] ) ;
487- this [ kUTF8FastPath ] = false ;
488- }
489+ const chunk = this [ kChunk ] ;
490+ const ignoreBom = this [ kIgnoreBOM ] || this [ kBOMSeen ] ;
491+ if ( ! stream ) {
492+ this [ kBOMSeen ] = false ;
493+ if ( ! chunk ) return decodeUTF8 ( input , ignoreBom , this [ kFatal ] ) ;
494+ }
495+
496+ let u = parseInput ( input ) ;
497+ if ( u . length === 0 && stream ) return '' ; // no state change
498+ let prefix ;
499+ if ( chunk ) {
500+ const merged = mergePrefixUtf8 ( u , this [ kChunk ] ) ;
501+ if ( u . length < 3 ) {
502+ u = merged ; // Might be unfinished, but fully consumed old u
503+ } else {
504+ prefix = merged ; // Stops at complete chunk
505+ const add = prefix . length - this [ kChunk ] . length ;
506+ if ( add > 0 ) u = u . subarray ( add ) ;
507+ }
508+
509+ this [ kChunk ] = null ;
510+ }
489511
490- this . #prepareConverter( ) ;
512+ if ( stream ) {
513+ const trail = unfinishedBytesUtf8 ( u , u . length ) ;
514+ if ( trail > 0 ) {
515+ this [ kChunk ] = new FastBuffer ( u . subarray ( - trail ) ) ; // copy
516+ if ( ! prefix && trail === u . length ) return '' ; // No further state change
517+ u = u . subarray ( 0 , - trail ) ;
518+ }
519+ }
520+
521+ try {
522+ const res = ( prefix ? decodeUTF8 ( prefix , ignoreBom , this [ kFatal ] ) : '' ) +
523+ decodeUTF8 ( u , ignoreBom || prefix , this [ kFatal ] ) ;
524+
525+ // "BOM seen" is set on the current decode call only if it did not error,
526+ // in "serialize I/O queue" after decoding
527+ // We don't get here if we had no complete data to process,
528+ // and we don't want BOM processing after that if streaming
529+ if ( stream ) this [ kBOMSeen ] = true ;
530+
531+ return res ;
532+ } catch ( e ) {
533+ this [ kChunk ] = null ; // Reset unfinished chunk on errors
534+ // The correct way per spec seems to be not destroying the decoder state (aka BOM here) in stream mode
535+ throw e ;
536+ }
537+ }
491538
492539 if ( hasIntl ) {
493540 const flags = stream ? 0 : CONVERTER_FLAGS_FLUSH ;
0 commit comments