@@ -202,6 +202,7 @@ class URLContext {
202
202
}
203
203
}
204
204
205
+ let setURLSearchParamsModified ;
205
206
let setURLSearchParamsContext ;
206
207
let getURLSearchParamsList ;
207
208
let setURLSearchParams ;
@@ -469,8 +470,9 @@ class URLSearchParams {
469
470
name = StringPrototypeToWellFormed ( `${ name } ` ) ;
470
471
value = StringPrototypeToWellFormed ( `${ value } ` ) ;
471
472
ArrayPrototypePush ( this . #searchParams, name , value ) ;
473
+
472
474
if ( this . #context) {
473
- this . #context. search = this . toString ( ) ;
475
+ setURLSearchParamsModified ( this . #context) ;
474
476
}
475
477
}
476
478
@@ -503,8 +505,9 @@ class URLSearchParams {
503
505
}
504
506
}
505
507
}
508
+
506
509
if ( this . #context) {
507
- this . #context. search = this . toString ( ) ;
510
+ setURLSearchParamsModified ( this . #context) ;
508
511
}
509
512
}
510
513
@@ -609,7 +612,7 @@ class URLSearchParams {
609
612
}
610
613
611
614
if ( this . #context) {
612
- this . #context. search = this . toString ( ) ;
615
+ setURLSearchParamsModified ( this . #context) ;
613
616
}
614
617
}
615
618
@@ -658,7 +661,7 @@ class URLSearchParams {
658
661
}
659
662
660
663
if ( this . #context) {
661
- this . #context. search = this . toString ( ) ;
664
+ setURLSearchParamsModified ( this . #context) ;
662
665
}
663
666
}
664
667
@@ -763,6 +766,20 @@ function isURL(self) {
763
766
class URL {
764
767
#context = new URLContext ( ) ;
765
768
#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
+ }
766
783
767
784
constructor ( input , base = undefined ) {
768
785
if ( arguments . length === 0 ) {
@@ -806,7 +823,37 @@ class URL {
806
823
return `${ constructor . name } ${ inspect ( obj , opts ) } ` ;
807
824
}
808
825
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
+
810
857
this . #context. href = href ;
811
858
812
859
const {
@@ -832,27 +879,39 @@ class URL {
832
879
this . #context. scheme_type = scheme_type ;
833
880
834
881
if ( 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
+ }
839
891
}
892
+
893
+ // If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
894
+ this . #ensureSearchParamsUpdated( ) ;
840
895
}
841
896
}
842
897
843
898
toString ( ) {
899
+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
900
+ this . #ensureSearchParamsUpdated( ) ;
844
901
return this . #context. href ;
845
902
}
846
903
847
904
get href ( ) {
905
+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
906
+ this . #ensureSearchParamsUpdated( ) ;
848
907
return this . #context. href ;
849
908
}
850
909
851
910
set href ( value ) {
852
911
value = `${ value } ` ;
853
912
const href = bindingUrl . update ( this . #context. href , updateActions . kHref , value ) ;
854
913
if ( ! href ) { throw new ERR_INVALID_URL ( value ) ; }
855
- this . #updateContext( href ) ;
914
+ this . #updateContext( href , true ) ;
856
915
}
857
916
858
917
// readonly
@@ -994,26 +1053,25 @@ class URL {
994
1053
}
995
1054
996
1055
get 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( ) ;
1002
1059
}
1003
1060
1004
1061
set search ( value ) {
1005
1062
const href = bindingUrl . update ( this . #context. href , updateActions . kSearch , StringPrototypeToWellFormed ( `${ value } ` ) ) ;
1006
1063
if ( href ) {
1007
- this . #updateContext( href ) ;
1064
+ this . #updateContext( href , true ) ;
1008
1065
}
1009
1066
}
1010
1067
1011
1068
// readonly
1012
1069
get searchParams ( ) {
1013
1070
// Create URLSearchParams on demand to greatly improve the URL performance.
1014
1071
if ( this . #searchParams == null ) {
1015
- this . #searchParams = new URLSearchParams ( this . search ) ;
1072
+ this . #searchParams = new URLSearchParams ( this . #getSearchFromContext ( ) ) ;
1016
1073
setURLSearchParamsContext ( this . #searchParams, this ) ;
1074
+ this . #searchParamsModified = false ;
1017
1075
}
1018
1076
return this . #searchParams;
1019
1077
}
@@ -1033,6 +1091,8 @@ class URL {
1033
1091
}
1034
1092
1035
1093
toJSON ( ) {
1094
+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1095
+ this . #ensureSearchParamsUpdated( ) ;
1036
1096
return this . #context. href ;
1037
1097
}
1038
1098
0 commit comments