url: remove #context from URLSearchParams
PR-URL: https://github.com/nodejs/node/pull/51520 Fixes: https://github.com/nodejs/node/issues/51518 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
a3abc42587
commit
925a464cb8
19
benchmark/url/url-searchparams-append.js
Normal file
19
benchmark/url/url-searchparams-append.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common.js');
|
||||||
|
|
||||||
|
const bench = common.createBenchmark(main, {
|
||||||
|
type: ['URL', 'URLSearchParams'],
|
||||||
|
n: [1e3, 1e6],
|
||||||
|
});
|
||||||
|
|
||||||
|
function main({ type, n }) {
|
||||||
|
const params = type === 'URL' ?
|
||||||
|
new URL('https://nodejs.org').searchParams :
|
||||||
|
new URLSearchParams();
|
||||||
|
|
||||||
|
bench.start();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
params.append('test', i);
|
||||||
|
}
|
||||||
|
bench.end(n);
|
||||||
|
}
|
29
benchmark/url/url-searchparams-update.js
Normal file
29
benchmark/url/url-searchparams-update.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common.js');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const bench = common.createBenchmark(main, {
|
||||||
|
searchParams: ['true', 'false'],
|
||||||
|
property: ['pathname', 'search', 'hash'],
|
||||||
|
n: [1e6],
|
||||||
|
});
|
||||||
|
|
||||||
|
function getMethod(url, property) {
|
||||||
|
if (property === 'pathname') return (x) => url.pathname = `/${x}`;
|
||||||
|
if (property === 'search') return (x) => url.search = `?${x}`;
|
||||||
|
if (property === 'hash') return (x) => url.hash = `#${x}`;
|
||||||
|
throw new Error(`Unsupported property "${property}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main({ searchParams, property, n }) {
|
||||||
|
const url = new URL('https://nodejs.org');
|
||||||
|
if (searchParams === 'true') assert(url.searchParams);
|
||||||
|
|
||||||
|
const method = getMethod(url, property);
|
||||||
|
|
||||||
|
bench.start();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
method(i);
|
||||||
|
}
|
||||||
|
bench.end(n);
|
||||||
|
}
|
@ -206,6 +206,7 @@ class URLContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let setURLSearchParamsModified;
|
||||||
let setURLSearchParamsContext;
|
let setURLSearchParamsContext;
|
||||||
let getURLSearchParamsList;
|
let getURLSearchParamsList;
|
||||||
let setURLSearchParams;
|
let setURLSearchParams;
|
||||||
@ -475,8 +476,9 @@ class URLSearchParams {
|
|||||||
name = StringPrototypeToWellFormed(`${name}`);
|
name = StringPrototypeToWellFormed(`${name}`);
|
||||||
value = StringPrototypeToWellFormed(`${value}`);
|
value = StringPrototypeToWellFormed(`${value}`);
|
||||||
ArrayPrototypePush(this.#searchParams, name, value);
|
ArrayPrototypePush(this.#searchParams, name, value);
|
||||||
|
|
||||||
if (this.#context) {
|
if (this.#context) {
|
||||||
this.#context.search = this.toString();
|
setURLSearchParamsModified(this.#context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,8 +511,9 @@ class URLSearchParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#context) {
|
if (this.#context) {
|
||||||
this.#context.search = this.toString();
|
setURLSearchParamsModified(this.#context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,7 +618,7 @@ class URLSearchParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.#context) {
|
if (this.#context) {
|
||||||
this.#context.search = this.toString();
|
setURLSearchParamsModified(this.#context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,7 +667,7 @@ class URLSearchParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.#context) {
|
if (this.#context) {
|
||||||
this.#context.search = this.toString();
|
setURLSearchParamsModified(this.#context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -769,6 +772,20 @@ function isURL(self) {
|
|||||||
class URL {
|
class URL {
|
||||||
#context = new URLContext();
|
#context = new URLContext();
|
||||||
#searchParams;
|
#searchParams;
|
||||||
|
#searchParamsModified;
|
||||||
|
|
||||||
|
static {
|
||||||
|
setURLSearchParamsModified = (obj) => {
|
||||||
|
// When URLSearchParams changes, we lazily update URL on the next read/write for performance.
|
||||||
|
obj.#searchParamsModified = true;
|
||||||
|
|
||||||
|
// If URL has an existing search, remove it without cascading back to URLSearchParams.
|
||||||
|
// Do this to avoid any internal confusion about whether URLSearchParams or URL is up-to-date.
|
||||||
|
if (obj.#context.hasSearch) {
|
||||||
|
obj.#updateContext(bindingUrl.update(obj.#context.href, updateActions.kSearch, ''));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
constructor(input, base = undefined) {
|
constructor(input, base = undefined) {
|
||||||
markTransferMode(this, false, false);
|
markTransferMode(this, false, false);
|
||||||
@ -814,7 +831,37 @@ class URL {
|
|||||||
return `${constructor.name} ${inspect(obj, opts)}`;
|
return `${constructor.name} ${inspect(obj, opts)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateContext(href) {
|
#getSearchFromContext() {
|
||||||
|
if (!this.#context.hasSearch) return '';
|
||||||
|
let endsAt = this.#context.href.length;
|
||||||
|
if (this.#context.hasHash) endsAt = this.#context.hash_start;
|
||||||
|
if (endsAt - this.#context.search_start <= 1) return '';
|
||||||
|
return StringPrototypeSlice(this.#context.href, this.#context.search_start, endsAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getSearchFromParams() {
|
||||||
|
if (!this.#searchParams?.size) return '';
|
||||||
|
return `?${this.#searchParams}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ensureSearchParamsUpdated() {
|
||||||
|
// URL is updated lazily to greatly improve performance when URLSearchParams is updated repeatedly.
|
||||||
|
// If URLSearchParams has been modified, reflect that back into URL, without cascading back.
|
||||||
|
if (this.#searchParamsModified) {
|
||||||
|
this.#searchParamsModified = false;
|
||||||
|
this.#updateContext(bindingUrl.update(this.#context.href, updateActions.kSearch, this.#getSearchFromParams()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the internal context state for URL.
|
||||||
|
* @param {string} href New href string from `bindingUrl.update`.
|
||||||
|
* @param {boolean} [shouldUpdateSearchParams] If the update has potential to update search params (href/search).
|
||||||
|
*/
|
||||||
|
#updateContext(href, shouldUpdateSearchParams = false) {
|
||||||
|
const previousSearch = shouldUpdateSearchParams && this.#searchParams &&
|
||||||
|
(this.#searchParamsModified ? this.#getSearchFromParams() : this.#getSearchFromContext());
|
||||||
|
|
||||||
this.#context.href = href;
|
this.#context.href = href;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -840,19 +887,31 @@ class URL {
|
|||||||
this.#context.scheme_type = scheme_type;
|
this.#context.scheme_type = scheme_type;
|
||||||
|
|
||||||
if (this.#searchParams) {
|
if (this.#searchParams) {
|
||||||
if (this.#context.hasSearch) {
|
// If the search string has updated, URL becomes the source of truth, and we update URLSearchParams.
|
||||||
setURLSearchParams(this.#searchParams, this.search);
|
// Only do this when we're expecting it to have changed, otherwise a change to hash etc.
|
||||||
} else {
|
// would incorrectly compare the URLSearchParams state to the empty URL search state.
|
||||||
setURLSearchParams(this.#searchParams, undefined);
|
if (shouldUpdateSearchParams) {
|
||||||
|
const currentSearch = this.#getSearchFromContext();
|
||||||
|
if (previousSearch !== currentSearch) {
|
||||||
|
setURLSearchParams(this.#searchParams, currentSearch);
|
||||||
|
this.#searchParamsModified = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
|
||||||
|
this.#ensureSearchParamsUpdated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
|
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
|
||||||
|
this.#ensureSearchParamsUpdated();
|
||||||
return this.#context.href;
|
return this.#context.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
get href() {
|
get href() {
|
||||||
|
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
|
||||||
|
this.#ensureSearchParamsUpdated();
|
||||||
return this.#context.href;
|
return this.#context.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -860,7 +919,7 @@ class URL {
|
|||||||
value = `${value}`;
|
value = `${value}`;
|
||||||
const href = bindingUrl.update(this.#context.href, updateActions.kHref, value);
|
const href = bindingUrl.update(this.#context.href, updateActions.kHref, value);
|
||||||
if (!href) { throw new ERR_INVALID_URL(value); }
|
if (!href) { throw new ERR_INVALID_URL(value); }
|
||||||
this.#updateContext(href);
|
this.#updateContext(href, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// readonly
|
// readonly
|
||||||
@ -1002,17 +1061,15 @@ class URL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get search() {
|
get search() {
|
||||||
if (!this.#context.hasSearch) { return ''; }
|
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
|
||||||
let endsAt = this.#context.href.length;
|
this.#ensureSearchParamsUpdated();
|
||||||
if (this.#context.hasHash) { endsAt = this.#context.hash_start; }
|
return this.#getSearchFromContext();
|
||||||
if (endsAt - this.#context.search_start <= 1) { return ''; }
|
|
||||||
return StringPrototypeSlice(this.#context.href, this.#context.search_start, endsAt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set search(value) {
|
set search(value) {
|
||||||
const href = bindingUrl.update(this.#context.href, updateActions.kSearch, StringPrototypeToWellFormed(`${value}`));
|
const href = bindingUrl.update(this.#context.href, updateActions.kSearch, StringPrototypeToWellFormed(`${value}`));
|
||||||
if (href) {
|
if (href) {
|
||||||
this.#updateContext(href);
|
this.#updateContext(href, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1020,8 +1077,9 @@ class URL {
|
|||||||
get searchParams() {
|
get searchParams() {
|
||||||
// Create URLSearchParams on demand to greatly improve the URL performance.
|
// Create URLSearchParams on demand to greatly improve the URL performance.
|
||||||
if (this.#searchParams == null) {
|
if (this.#searchParams == null) {
|
||||||
this.#searchParams = new URLSearchParams(this.search);
|
this.#searchParams = new URLSearchParams(this.#getSearchFromContext());
|
||||||
setURLSearchParamsContext(this.#searchParams, this);
|
setURLSearchParamsContext(this.#searchParams, this);
|
||||||
|
this.#searchParamsModified = false;
|
||||||
}
|
}
|
||||||
return this.#searchParams;
|
return this.#searchParams;
|
||||||
}
|
}
|
||||||
@ -1041,6 +1099,8 @@ class URL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
// Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
|
||||||
|
this.#ensureSearchParamsUpdated();
|
||||||
return this.#context.href;
|
return this.#context.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,42 @@ assert.strictEqual(sp.toString(), serialized);
|
|||||||
|
|
||||||
assert.strictEqual(m.search, `?${serialized}`);
|
assert.strictEqual(m.search, `?${serialized}`);
|
||||||
|
|
||||||
|
sp.delete('a');
|
||||||
|
values.forEach((i) => sp.append('a', i));
|
||||||
|
assert.strictEqual(m.href, `http://example.org/?${serialized}`);
|
||||||
|
|
||||||
|
sp.delete('a');
|
||||||
|
values.forEach((i) => sp.append('a', i));
|
||||||
|
assert.strictEqual(m.toString(), `http://example.org/?${serialized}`);
|
||||||
|
|
||||||
|
sp.delete('a');
|
||||||
|
values.forEach((i) => sp.append('a', i));
|
||||||
|
assert.strictEqual(m.toJSON(), `http://example.org/?${serialized}`);
|
||||||
|
|
||||||
|
sp.delete('a');
|
||||||
|
values.forEach((i) => sp.append('a', i));
|
||||||
|
m.href = 'http://example.org';
|
||||||
|
assert.strictEqual(m.href, 'http://example.org/');
|
||||||
|
assert.strictEqual(sp.size, 0);
|
||||||
|
|
||||||
|
sp.delete('a');
|
||||||
|
values.forEach((i) => sp.append('a', i));
|
||||||
|
m.search = '';
|
||||||
|
assert.strictEqual(m.href, 'http://example.org/');
|
||||||
|
assert.strictEqual(sp.size, 0);
|
||||||
|
|
||||||
|
sp.delete('a');
|
||||||
|
values.forEach((i) => sp.append('a', i));
|
||||||
|
m.pathname = '/test';
|
||||||
|
assert.strictEqual(m.href, `http://example.org/test?${serialized}`);
|
||||||
|
m.pathname = '';
|
||||||
|
|
||||||
|
sp.delete('a');
|
||||||
|
values.forEach((i) => sp.append('a', i));
|
||||||
|
m.hash = '#test';
|
||||||
|
assert.strictEqual(m.href, `http://example.org/?${serialized}#test`);
|
||||||
|
m.hash = '';
|
||||||
|
|
||||||
assert.strictEqual(sp[Symbol.iterator], sp.entries);
|
assert.strictEqual(sp[Symbol.iterator], sp.entries);
|
||||||
|
|
||||||
let key, val;
|
let key, val;
|
||||||
|
@ -11,12 +11,26 @@ const assert = require('assert');
|
|||||||
].forEach((i) => {
|
].forEach((i) => {
|
||||||
assert.throws(() => Reflect.apply(URL.prototype[i], [], {}), {
|
assert.throws(() => Reflect.apply(URL.prototype[i], [], {}), {
|
||||||
name: 'TypeError',
|
name: 'TypeError',
|
||||||
message: /Cannot read private member/,
|
message: /Receiver must be an instance of class/,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
'href',
|
'href',
|
||||||
|
'search',
|
||||||
|
].forEach((i) => {
|
||||||
|
assert.throws(() => Reflect.get(URL.prototype, i, {}), {
|
||||||
|
name: 'TypeError',
|
||||||
|
message: /Receiver must be an instance of class/,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => Reflect.set(URL.prototype, i, null, {}), {
|
||||||
|
name: 'TypeError',
|
||||||
|
message: /Cannot read private member/,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
'protocol',
|
'protocol',
|
||||||
'username',
|
'username',
|
||||||
'password',
|
'password',
|
||||||
@ -24,7 +38,6 @@ const assert = require('assert');
|
|||||||
'hostname',
|
'hostname',
|
||||||
'port',
|
'port',
|
||||||
'pathname',
|
'pathname',
|
||||||
'search',
|
|
||||||
'hash',
|
'hash',
|
||||||
].forEach((i) => {
|
].forEach((i) => {
|
||||||
assert.throws(() => Reflect.get(URL.prototype, i, {}), {
|
assert.throws(() => Reflect.get(URL.prototype, i, {}), {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user