Skip to content

Commit 4bc8e98

Browse files
MattIPv4richardlau
authored andcommitted
url: don't update URL immediately on update to URLSearchParams
PR-URL: #51520Fixes: #51518 Backport-PR-URL: #51559 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 0211a3d commit 4bc8e98

File tree

5 files changed

+176
-19
lines changed

5 files changed

+176
-19
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
4+
constbench=common.createBenchmark(main,{
5+
type: ['URL','URLSearchParams'],
6+
n: [1e3,1e6],
7+
});
8+
9+
functionmain({ type, n }){
10+
constparams=type==='URL' ?
11+
newURL('https://nodejs.org').searchParams :
12+
newURLSearchParams();
13+
14+
bench.start();
15+
for(leti=0;i<n;i++){
16+
params.append('test',i);
17+
}
18+
bench.end(n);
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
constassert=require('assert');
4+
5+
constbench=common.createBenchmark(main,{
6+
searchParams: ['true','false'],
7+
property: ['pathname','search','hash'],
8+
n: [1e6],
9+
});
10+
11+
functiongetMethod(url,property){
12+
if(property==='pathname')return(x)=>url.pathname=`/${x}`;
13+
if(property==='search')return(x)=>url.search=`?${x}`;
14+
if(property==='hash')return(x)=>url.hash=`#${x}`;
15+
thrownewError(`Unsupported property "${property}"`);
16+
}
17+
18+
functionmain({ searchParams, property, n }){
19+
consturl=newURL('https://nodejs.org');
20+
if(searchParams==='true')assert(url.searchParams);
21+
22+
constmethod=getMethod(url,property);
23+
24+
bench.start();
25+
for(leti=0;i<n;i++){
26+
method(i);
27+
}
28+
bench.end(n);
29+
}

‎lib/internal/url.js‎

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ class URLContext{
202202
}
203203
}
204204

205+
letsetURLSearchParamsModified;
205206
letsetURLSearchParamsContext;
206207
letgetURLSearchParamsList;
207208
letsetURLSearchParams;
@@ -469,8 +470,9 @@ class URLSearchParams{
469470
name=StringPrototypeToWellFormed(`${name}`);
470471
value=StringPrototypeToWellFormed(`${value}`);
471472
ArrayPrototypePush(this.#searchParams,name,value);
473+
472474
if(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+
506509
if(this.#context){
507-
this.#context.search=this.toString();
510+
setURLSearchParamsModified(this.#context);
508511
}
509512
}
510513

@@ -609,7 +612,7 @@ class URLSearchParams{
609612
}
610613

611614
if(this.#context){
612-
this.#context.search=this.toString();
615+
setURLSearchParamsModified(this.#context);
613616
}
614617
}
615618

@@ -658,7 +661,7 @@ class URLSearchParams{
658661
}
659662

660663
if(this.#context){
661-
this.#context.search=this.toString();
664+
setURLSearchParamsModified(this.#context);
662665
}
663666
}
664667

@@ -763,6 +766,20 @@ function isURL(self){
763766
classURL{
764767
#context =newURLContext();
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

767784
constructor(input,base=undefined){
768785
if(arguments.length===0){
@@ -806,7 +823,37 @@ class URL{
806823
return`${constructor.name}${inspect(obj,opts)}`;
807824
}
808825

809-
#updateContext(href){
826+
#getSearchFromContext(){
827+
if(!this.#context.hasSearch)return'';
828+
letendsAt=this.#context.href.length;
829+
if(this.#context.hasHash)endsAt=this.#context.hash_start;
830+
if(endsAt-this.#context.search_start<=1)return'';
831+
returnStringPrototypeSlice(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+
constpreviousSearch=shouldUpdateSearchParams&&this.#searchParams &&
855+
(this.#searchParamsModified ? this.#getSearchFromParams() : this.#getSearchFromContext());
856+
810857
this.#context.href=href;
811858

812859
const{
@@ -832,27 +879,39 @@ class URL{
832879
this.#context.scheme_type=scheme_type;
833880

834881
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+
constcurrentSearch=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

843898
toString(){
899+
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
900+
this.#ensureSearchParamsUpdated();
844901
returnthis.#context.href;
845902
}
846903

847904
gethref(){
905+
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
906+
this.#ensureSearchParamsUpdated();
848907
returnthis.#context.href;
849908
}
850909

851910
sethref(value){
852911
value=`${value}`;
853912
consthref=bindingUrl.update(this.#context.href,updateActions.kHref,value);
854913
if(!href){thrownewERR_INVALID_URL(value);}
855-
this.#updateContext(href);
914+
this.#updateContext(href,true);
856915
}
857916

858917
// readonly
@@ -994,26 +1053,25 @@ class URL{
9941053
}
9951054

9961055
getsearch(){
997-
if(!this.#context.hasSearch){return'';}
998-
letendsAt=this.#context.href.length;
999-
if(this.#context.hasHash){endsAt=this.#context.hash_start;}
1000-
if(endsAt-this.#context.search_start<=1){return'';}
1001-
returnStringPrototypeSlice(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+
returnthis.#getSearchFromContext();
10021059
}
10031060

10041061
setsearch(value){
10051062
consthref=bindingUrl.update(this.#context.href,updateActions.kSearch,StringPrototypeToWellFormed(`${value}`));
10061063
if(href){
1007-
this.#updateContext(href);
1064+
this.#updateContext(href,true);
10081065
}
10091066
}
10101067

10111068
// readonly
10121069
getsearchParams(){
10131070
// Create URLSearchParams on demand to greatly improve the URL performance.
10141071
if(this.#searchParams ==null){
1015-
this.#searchParams =newURLSearchParams(this.search);
1072+
this.#searchParams =newURLSearchParams(this.#getSearchFromContext());
10161073
setURLSearchParamsContext(this.#searchParams,this);
1074+
this.#searchParamsModified =false;
10171075
}
10181076
returnthis.#searchParams;
10191077
}
@@ -1033,6 +1091,8 @@ class URL{
10331091
}
10341092

10351093
toJSON(){
1094+
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1095+
this.#ensureSearchParamsUpdated();
10361096
returnthis.#context.href;
10371097
}
10381098

‎test/parallel/test-whatwg-url-custom-searchparams.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,42 @@ assert.strictEqual(sp.toString(), serialized);
4343

4444
assert.strictEqual(m.search,`?${serialized}`);
4545

46+
sp.delete('a');
47+
values.forEach((i)=>sp.append('a',i));
48+
assert.strictEqual(m.href,`http://example.org/?${serialized}`);
49+
50+
sp.delete('a');
51+
values.forEach((i)=>sp.append('a',i));
52+
assert.strictEqual(m.toString(),`http://example.org/?${serialized}`);
53+
54+
sp.delete('a');
55+
values.forEach((i)=>sp.append('a',i));
56+
assert.strictEqual(m.toJSON(),`http://example.org/?${serialized}`);
57+
58+
sp.delete('a');
59+
values.forEach((i)=>sp.append('a',i));
60+
m.href='http://example.org';
61+
assert.strictEqual(m.href,'http://example.org/');
62+
assert.strictEqual(sp.size,0);
63+
64+
sp.delete('a');
65+
values.forEach((i)=>sp.append('a',i));
66+
m.search='';
67+
assert.strictEqual(m.href,'http://example.org/');
68+
assert.strictEqual(sp.size,0);
69+
70+
sp.delete('a');
71+
values.forEach((i)=>sp.append('a',i));
72+
m.pathname='/test';
73+
assert.strictEqual(m.href,`http://example.org/test?${serialized}`);
74+
m.pathname='';
75+
76+
sp.delete('a');
77+
values.forEach((i)=>sp.append('a',i));
78+
m.hash='#test';
79+
assert.strictEqual(m.href,`http://example.org/?${serialized}#test`);
80+
m.hash='';
81+
4682
assert.strictEqual(sp[Symbol.iterator],sp.entries);
4783

4884
letkey,val;

‎test/parallel/test-whatwg-url-invalidthis.js‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,33 @@ const assert = require('assert');
1111
].forEach((i)=>{
1212
assert.throws(()=>Reflect.apply(URL.prototype[i],[],{}),{
1313
name: 'TypeError',
14-
message: /Cannotreadprivatemember/,
14+
message: /Receivermustbeaninstanceofclass/,
1515
});
1616
});
1717

1818
[
1919
'href',
20+
'search',
21+
].forEach((i)=>{
22+
assert.throws(()=>Reflect.get(URL.prototype,i,{}),{
23+
name: 'TypeError',
24+
message: /Receivermustbeaninstanceofclass/,
25+
});
26+
27+
assert.throws(()=>Reflect.set(URL.prototype,i,null,{}),{
28+
name: 'TypeError',
29+
message: /Cannotreadprivatemember/,
30+
});
31+
});
32+
33+
[
2034
'protocol',
2135
'username',
2236
'password',
2337
'host',
2438
'hostname',
2539
'port',
2640
'pathname',
27-
'search',
2841
'hash',
2942
].forEach((i)=>{
3043
assert.throws(()=>Reflect.get(URL.prototype,i,{}),{

0 commit comments

Comments
(0)