@@ -206,6 +206,7 @@ class URLContext {
206
206
}
207
207
}
208
208
209
+ let setURLSearchParamsModified ;
209
210
let setURLSearchParamsContext ;
210
211
let getURLSearchParamsList ;
211
212
let setURLSearchParams ;
@@ -475,8 +476,9 @@ class URLSearchParams {
475
476
name = StringPrototypeToWellFormed ( `${ name } ` ) ;
476
477
value = StringPrototypeToWellFormed ( `${ value } ` ) ;
477
478
ArrayPrototypePush ( this . #searchParams, name , value ) ;
479
+
478
480
if ( this . #context) {
479
- this . #context. search = this . toString ( ) ;
481
+ setURLSearchParamsModified ( this . #context) ;
480
482
}
481
483
}
482
484
@@ -509,8 +511,9 @@ class URLSearchParams {
509
511
}
510
512
}
511
513
}
514
+
512
515
if ( this . #context) {
513
- this . #context. search = this . toString ( ) ;
516
+ setURLSearchParamsModified ( this . #context) ;
514
517
}
515
518
}
516
519
@@ -615,7 +618,7 @@ class URLSearchParams {
615
618
}
616
619
617
620
if ( this . #context) {
618
- this . #context. search = this . toString ( ) ;
621
+ setURLSearchParamsModified ( this . #context) ;
619
622
}
620
623
}
621
624
@@ -664,7 +667,7 @@ class URLSearchParams {
664
667
}
665
668
666
669
if ( this . #context) {
667
- this . #context. search = this . toString ( ) ;
670
+ setURLSearchParamsModified ( this . #context) ;
668
671
}
669
672
}
670
673
@@ -769,6 +772,20 @@ function isURL(self) {
769
772
class URL {
770
773
#context = new URLContext ( ) ;
771
774
#searchParams;
775
+ #searchParamsModified;
776
+
777
+ static {
778
+ setURLSearchParamsModified = ( obj ) => {
779
+ // When URLSearchParams changes, we lazily update URL on the next read/write for performance.
780
+ obj . #searchParamsModified = true ;
781
+
782
+ // If URL has an existing search, remove it without cascading back to URLSearchParams.
783
+ // Do this to avoid any internal confusion about whether URLSearchParams or URL is up-to-date.
784
+ if ( obj . #context. hasSearch ) {
785
+ obj . #updateContext( bindingUrl . update ( obj . #context. href , updateActions . kSearch , '' ) ) ;
786
+ }
787
+ } ;
788
+ }
772
789
773
790
constructor ( input , base = undefined ) {
774
791
markTransferMode ( this , false , false ) ;
@@ -814,7 +831,37 @@ class URL {
814
831
return `${ constructor . name } ${ inspect ( obj , opts ) } ` ;
815
832
}
816
833
817
- #updateContext( href ) {
834
+ #getSearchFromContext( ) {
835
+ if ( ! this . #context. hasSearch ) return '' ;
836
+ let endsAt = this . #context. href . length ;
837
+ if ( this . #context. hasHash ) endsAt = this . #context. hash_start ;
838
+ if ( endsAt - this . #context. search_start <= 1 ) return '' ;
839
+ return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
840
+ }
841
+
842
+ #getSearchFromParams( ) {
843
+ if ( ! this . #searchParams?. size ) return '' ;
844
+ return `?${ this . #searchParams} ` ;
845
+ }
846
+
847
+ #ensureSearchParamsUpdated( ) {
848
+ // URL is updated lazily to greatly improve performance when URLSearchParams is updated repeatedly.
849
+ // If URLSearchParams has been modified, reflect that back into URL, without cascading back.
850
+ if ( this . #searchParamsModified) {
851
+ this . #searchParamsModified = false ;
852
+ this . #updateContext( bindingUrl . update ( this . #context. href , updateActions . kSearch , this . #getSearchFromParams( ) ) ) ;
853
+ }
854
+ }
855
+
856
+ /**
857
+ * Update the internal context state for URL.
858
+ * @param {string } href New href string from `bindingUrl.update`.
859
+ * @param {boolean } [shouldUpdateSearchParams] If the update has potential to update search params (href/search).
860
+ */
861
+ #updateContext( href , shouldUpdateSearchParams = false ) {
862
+ const previousSearch = shouldUpdateSearchParams && this . #searchParams &&
863
+ ( this . #searchParamsModified ? this . #getSearchFromParams( ) : this . #getSearchFromContext( ) ) ;
864
+
818
865
this . #context. href = href ;
819
866
820
867
const {
@@ -840,27 +887,39 @@ class URL {
840
887
this . #context. scheme_type = scheme_type ;
841
888
842
889
if ( this . #searchParams) {
843
- if ( this . #context. hasSearch ) {
844
- setURLSearchParams ( this . #searchParams, this . search ) ;
845
- } else {
846
- setURLSearchParams ( this . #searchParams, undefined ) ;
890
+ // If the search string has updated, URL becomes the source of truth, and we update URLSearchParams.
891
+ // Only do this when we're expecting it to have changed, otherwise a change to hash etc.
892
+ // would incorrectly compare the URLSearchParams state to the empty URL search state.
893
+ if ( shouldUpdateSearchParams ) {
894
+ const currentSearch = this . #getSearchFromContext( ) ;
895
+ if ( previousSearch !== currentSearch ) {
896
+ setURLSearchParams ( this . #searchParams, currentSearch ) ;
897
+ this . #searchParamsModified = false ;
898
+ }
847
899
}
900
+
901
+ // If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
902
+ this . #ensureSearchParamsUpdated( ) ;
848
903
}
849
904
}
850
905
851
906
toString ( ) {
907
+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
908
+ this . #ensureSearchParamsUpdated( ) ;
852
909
return this . #context. href ;
853
910
}
854
911
855
912
get href ( ) {
913
+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
914
+ this . #ensureSearchParamsUpdated( ) ;
856
915
return this . #context. href ;
857
916
}
858
917
859
918
set href ( value ) {
860
919
value = `${ value } ` ;
861
920
const href = bindingUrl . update ( this . #context. href , updateActions . kHref , value ) ;
862
921
if ( ! href ) { throw new ERR_INVALID_URL ( value ) ; }
863
- this . #updateContext( href ) ;
922
+ this . #updateContext( href , true ) ;
864
923
}
865
924
866
925
// readonly
@@ -1002,26 +1061,25 @@ class URL {
1002
1061
}
1003
1062
1004
1063
get search ( ) {
1005
- if ( ! this . #context. hasSearch ) { return '' ; }
1006
- let endsAt = this . #context. href . length ;
1007
- if ( this . #context. hasHash ) { endsAt = this . #context. hash_start ; }
1008
- if ( endsAt - this . #context. search_start <= 1 ) { return '' ; }
1009
- return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
1064
+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1065
+ this . #ensureSearchParamsUpdated( ) ;
1066
+ return this . #getSearchFromContext( ) ;
1010
1067
}
1011
1068
1012
1069
set search ( value ) {
1013
1070
const href = bindingUrl . update ( this . #context. href , updateActions . kSearch , StringPrototypeToWellFormed ( `${ value } ` ) ) ;
1014
1071
if ( href ) {
1015
- this . #updateContext( href ) ;
1072
+ this . #updateContext( href , true ) ;
1016
1073
}
1017
1074
}
1018
1075
1019
1076
// readonly
1020
1077
get searchParams ( ) {
1021
1078
// Create URLSearchParams on demand to greatly improve the URL performance.
1022
1079
if ( this . #searchParams == null ) {
1023
- this . #searchParams = new URLSearchParams ( this . search ) ;
1080
+ this . #searchParams = new URLSearchParams ( this . #getSearchFromContext ( ) ) ;
1024
1081
setURLSearchParamsContext ( this . #searchParams, this ) ;
1082
+ this . #searchParamsModified = false ;
1025
1083
}
1026
1084
return this . #searchParams;
1027
1085
}
@@ -1041,6 +1099,8 @@ class URL {
1041
1099
}
1042
1100
1043
1101
toJSON ( ) {
1102
+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1103
+ this . #ensureSearchParamsUpdated( ) ;
1044
1104
return this . #context. href ;
1045
1105
}
1046
1106
0 commit comments