@@ -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,6 +449,7 @@ 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 ;
@@ -483,8 +486,48 @@ 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 ;
489+ const chunk = this [ kChunk ] ;
490+ let 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+ }
511+
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 ] ) : '' ) + decodeUTF8 ( u , ignoreBom || prefix , this [ kFatal ] ) ;
523+ // "BOM seen" is set on the current decode call only if it did not error, in "serialize I/O queue" after decoding
524+ // We don't get here if we had no complete data to process, and we don't want BOM processing after that if streaming
525+ if ( stream ) this [ kBOMSeen ] = true
526+ } catch ( e ) {
527+ this [ kChunk ] = null // reset unfinished chunk on errors
528+ // The correct way per spec seems to be not destroying the decoder state (aka BOM here) in stream mode
529+ throw e
530+ }
488531 }
489532
490533 this . #prepareConverter( ) ;
0 commit comments