@@ -202,6 +202,7 @@ class URLContext{
202202}
203203}
204204
205+ let setURLSearchParamsModified ;
205206let setURLSearchParamsContext ;
206207let getURLSearchParamsList ;
207208let setURLSearchParams ;
@@ -469,8 +470,9 @@ class URLSearchParams{
469470name = StringPrototypeToWellFormed ( `${ name } ` ) ;
470471value = StringPrototypeToWellFormed ( `${ value } ` ) ;
471472ArrayPrototypePush ( this . #searchParams, name , value ) ;
473+
472474if ( this . #context) {
473- this . #context. search = this . toString ( ) ;
475+ setURLSearchParamsModified ( this . #context) ;
474476}
475477}
476478
@@ -503,8 +505,9 @@ class URLSearchParams{
503505}
504506}
505507}
508+
506509if ( this . #context) {
507- this . #context. search = this . toString ( ) ;
510+ setURLSearchParamsModified ( this . #context) ;
508511}
509512}
510513
@@ -609,7 +612,7 @@ class URLSearchParams{
609612}
610613
611614if ( this . #context) {
612- this . #context. search = this . toString ( ) ;
615+ setURLSearchParamsModified ( this . #context) ;
613616}
614617}
615618
@@ -658,7 +661,7 @@ class URLSearchParams{
658661}
659662
660663if ( this . #context) {
661- this . #context. search = this . toString ( ) ;
664+ setURLSearchParamsModified ( this . #context) ;
662665}
663666}
664667
@@ -763,6 +766,20 @@ function isURL(self){
763766class URL {
764767 #context = new URLContext ( ) ;
765768 #searchParams;
769+ #searchParamsModified;
770+
771+ static {
772+ setURLSearchParamsModified = ( obj ) => {
773+ // When URLSearchParams changes, we lazily update URL on the next read/write for performance.
774+ obj . #searchParamsModified = true ;
775+
776+ // If URL has an existing search, remove it without cascading back to URLSearchParams.
777+ // Do this to avoid any internal confusion about whether URLSearchParams or URL is up-to-date.
778+ if ( obj . #context. hasSearch ) {
779+ obj . #updateContext( bindingUrl . update ( obj . #context. href , updateActions . kSearch , '' ) ) ;
780+ }
781+ } ;
782+ }
766783
767784constructor ( input , base = undefined ) {
768785if ( arguments . length === 0 ) {
@@ -806,7 +823,37 @@ class URL{
806823return `${ constructor . name } ${ inspect ( obj , opts ) } ` ;
807824}
808825
809- #updateContext( href ) {
826+ #getSearchFromContext( ) {
827+ if ( ! this . #context. hasSearch ) return '' ;
828+ let endsAt = this . #context. href . length ;
829+ if ( this . #context. hasHash ) endsAt = this . #context. hash_start ;
830+ if ( endsAt - this . #context. search_start <= 1 ) return '' ;
831+ return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
832+ }
833+
834+ #getSearchFromParams( ) {
835+ if ( ! this . #searchParams?. size ) return '' ;
836+ return `?${ this . #searchParams} ` ;
837+ }
838+
839+ #ensureSearchParamsUpdated( ) {
840+ // URL is updated lazily to greatly improve performance when URLSearchParams is updated repeatedly.
841+ // If URLSearchParams has been modified, reflect that back into URL, without cascading back.
842+ if ( this . #searchParamsModified) {
843+ this . #searchParamsModified = false ;
844+ this . #updateContext( bindingUrl . update ( this . #context. href , updateActions . kSearch , this . #getSearchFromParams( ) ) ) ;
845+ }
846+ }
847+
848+ /**
849+ * Update the internal context state for URL.
850+ * @param {string } href New href string from `bindingUrl.update`.
851+ * @param {boolean } [shouldUpdateSearchParams] If the update has potential to update search params (href/search).
852+ */
853+ #updateContext( href , shouldUpdateSearchParams = false ) {
854+ const previousSearch = shouldUpdateSearchParams && this . #searchParams &&
855+ ( this . #searchParamsModified ? this . #getSearchFromParams( ) : this . #getSearchFromContext( ) ) ;
856+
810857this . #context. href = href ;
811858
812859const {
@@ -832,27 +879,39 @@ class URL{
832879this . #context. scheme_type = scheme_type ;
833880
834881if ( this . #searchParams) {
835- if ( this . #context. hasSearch ) {
836- setURLSearchParams ( this . #searchParams, this . search ) ;
837- } else {
838- setURLSearchParams ( this . #searchParams, undefined ) ;
882+ // If the search string has updated, URL becomes the source of truth, and we update URLSearchParams.
883+ // Only do this when we're expecting it to have changed, otherwise a change to hash etc.
884+ // would incorrectly compare the URLSearchParams state to the empty URL search state.
885+ if ( shouldUpdateSearchParams ) {
886+ const currentSearch = this . #getSearchFromContext( ) ;
887+ if ( previousSearch !== currentSearch ) {
888+ setURLSearchParams ( this . #searchParams, currentSearch ) ;
889+ this . #searchParamsModified = false ;
890+ }
839891}
892+
893+ // If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
894+ this . #ensureSearchParamsUpdated( ) ;
840895}
841896}
842897
843898toString ( ) {
899+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
900+ this . #ensureSearchParamsUpdated( ) ;
844901return this . #context. href ;
845902}
846903
847904get href ( ) {
905+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
906+ this . #ensureSearchParamsUpdated( ) ;
848907return this . #context. href ;
849908}
850909
851910set href ( value ) {
852911value = `${ value } ` ;
853912const href = bindingUrl . update ( this . #context. href , updateActions . kHref , value ) ;
854913if ( ! href ) { throw new ERR_INVALID_URL ( value ) ; }
855- this . #updateContext( href ) ;
914+ this . #updateContext( href , true ) ;
856915}
857916
858917// readonly
@@ -994,26 +1053,25 @@ class URL{
9941053}
9951054
9961055get search ( ) {
997- if ( ! this . #context. hasSearch ) { return '' ; }
998- let endsAt = this . #context. href . length ;
999- if ( this . #context. hasHash ) { endsAt = this . #context. hash_start ; }
1000- if ( endsAt - this . #context. search_start <= 1 ) { return '' ; }
1001- return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
1056+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1057+ this . #ensureSearchParamsUpdated( ) ;
1058+ return this . #getSearchFromContext( ) ;
10021059}
10031060
10041061set search ( value ) {
10051062const href = bindingUrl . update ( this . #context. href , updateActions . kSearch , StringPrototypeToWellFormed ( `${ value } ` ) ) ;
10061063if ( href ) {
1007- this . #updateContext( href ) ;
1064+ this . #updateContext( href , true ) ;
10081065}
10091066}
10101067
10111068// readonly
10121069get searchParams ( ) {
10131070// Create URLSearchParams on demand to greatly improve the URL performance.
10141071if ( this . #searchParams == null ) {
1015- this . #searchParams = new URLSearchParams ( this . search ) ;
1072+ this . #searchParams = new URLSearchParams ( this . #getSearchFromContext ( ) ) ;
10161073setURLSearchParamsContext ( this . #searchParams, this ) ;
1074+ this . #searchParamsModified = false ;
10171075}
10181076return this . #searchParams;
10191077}
@@ -1033,6 +1091,8 @@ class URL{
10331091}
10341092
10351093toJSON ( ) {
1094+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1095+ this . #ensureSearchParamsUpdated( ) ;
10361096return this . #context. href ;
10371097}
10381098
0 commit comments