@@ -206,6 +206,7 @@ class URLContext {
206206 }
207207}
208208
209+ let setURLSearchParamsModified;
209210let setURLSearchParamsContext;
210211let getURLSearchParamsList;
211212let setURLSearchParams;
@@ -475,8 +476,9 @@ class URLSearchParams {
475476 name = StringPrototypeToWellFormed(`${name}`);
476477 value = StringPrototypeToWellFormed(`${value}`);
477478 ArrayPrototypePush(this.#searchParams, name, value);
479+
478480 if (this.#context) {
479- this.#context.search = this.toString( );
481+ setURLSearchParamsModified( this.#context);
480482 }
481483 }
482484
@@ -509,8 +511,9 @@ class URLSearchParams {
509511 }
510512 }
511513 }
514+
512515 if (this.#context) {
513- this.#context.search = this.toString( );
516+ setURLSearchParamsModified( this.#context);
514517 }
515518 }
516519
@@ -615,7 +618,7 @@ class URLSearchParams {
615618 }
616619
617620 if (this.#context) {
618- this.#context.search = this.toString( );
621+ setURLSearchParamsModified( this.#context);
619622 }
620623 }
621624
@@ -664,7 +667,7 @@ class URLSearchParams {
664667 }
665668
666669 if (this.#context) {
667- this.#context.search = this.toString( );
670+ setURLSearchParamsModified( this.#context);
668671 }
669672 }
670673
@@ -769,6 +772,20 @@ function isURL(self) {
769772class URL {
770773 #context = new URLContext();
771774 #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+ }
772789
773790 constructor(input, base = undefined) {
774791 markTransferMode(this, false, false);
@@ -814,7 +831,37 @@ class URL {
814831 return `${constructor.name} ${inspect(obj, opts)}`;
815832 }
816833
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+
818865 this.#context.href = href;
819866
820867 const {
@@ -840,27 +887,39 @@ class URL {
840887 this.#context.scheme_type = scheme_type;
841888
842889 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+ }
847899 }
900+
901+ // If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
902+ this.#ensureSearchParamsUpdated();
848903 }
849904 }
850905
851906 toString() {
907+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
908+ this.#ensureSearchParamsUpdated();
852909 return this.#context.href;
853910 }
854911
855912 get href() {
913+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
914+ this.#ensureSearchParamsUpdated();
856915 return this.#context.href;
857916 }
858917
859918 set href(value) {
860919 value = `${value}`;
861920 const href = bindingUrl.update(this.#context.href, updateActions.kHref, value);
862921 if (!href) { throw new ERR_INVALID_URL(value); }
863- this.#updateContext(href);
922+ this.#updateContext(href, true );
864923 }
865924
866925 // readonly
@@ -1002,26 +1061,25 @@ class URL {
10021061 }
10031062
10041063 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();
10101067 }
10111068
10121069 set search(value) {
10131070 const href = bindingUrl.update(this.#context.href, updateActions.kSearch, StringPrototypeToWellFormed(`${value}`));
10141071 if (href) {
1015- this.#updateContext(href);
1072+ this.#updateContext(href, true );
10161073 }
10171074 }
10181075
10191076 // readonly
10201077 get searchParams() {
10211078 // Create URLSearchParams on demand to greatly improve the URL performance.
10221079 if (this.#searchParams == null) {
1023- this.#searchParams = new URLSearchParams(this.search );
1080+ this.#searchParams = new URLSearchParams(this.#getSearchFromContext() );
10241081 setURLSearchParamsContext(this.#searchParams, this);
1082+ this.#searchParamsModified = false;
10251083 }
10261084 return this.#searchParams;
10271085 }
@@ -1041,6 +1099,8 @@ class URL {
10411099 }
10421100
10431101 toJSON() {
1102+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1103+ this.#ensureSearchParamsUpdated();
10441104 return this.#context.href;
10451105 }
10461106
0 commit comments