| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 0211a3d commit 4bc8e98
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,19 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + const common = require('../common.js'); | ||
| 3 | + | ||
| 4 | + const bench = common.createBenchmark(main, { | ||
| 5 | + type: ['URL', 'URLSearchParams'], | ||
| 6 | + n: [1e3, 1e6], | ||
| 7 | + }); | ||
| 8 | + | ||
| 9 | + function main({ type, n }) { | ||
| 10 | + const params = type === 'URL' ? | ||
| 11 | + new URL('https://nodejs.org').searchParams : | ||
| 12 | + new URLSearchParams(); | ||
| 13 | + | ||
| 14 | + bench.start(); | ||
| 15 | + for (let i = 0; i < n; i++) { | ||
| 16 | + params.append('test', i); | ||
| 17 | + } | ||
| 18 | + bench.end(n); | ||
| 19 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,29 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + const common = require('../common.js'); | ||
| 3 | + const assert = require('assert'); | ||
| 4 | + | ||
| 5 | + const bench = common.createBenchmark(main, { | ||
| 6 | + searchParams: ['true', 'false'], | ||
| 7 | + property: ['pathname', 'search', 'hash'], | ||
| 8 | + n: [1e6], | ||
| 9 | + }); | ||
| 10 | + | ||
| 11 | + function getMethod(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 | + throw new Error(`Unsupported property "${property}"`); | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + function main({ searchParams, property, n }) { | ||
| 19 | + const url = new URL('https://nodejs.org'); | ||
| 20 | + if (searchParams === 'true') assert(url.searchParams); | ||
| 21 | + | ||
| 22 | + const method = getMethod(url, property); | ||
| 23 | + | ||
| 24 | + bench.start(); | ||
| 25 | + for (let i = 0; i < n; i++) { | ||
| 26 | + method(i); | ||
| 27 | + } | ||
| 28 | + bench.end(n); | ||
| 29 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -43,6 +43,42 @@ assert.strictEqual(sp.toString(), serialized); | |||
| 43 | 43 | ||
| 44 | 44 | assert.strictEqual(m.search, `?${serialized}`); | |
| 45 | 45 | ||
| 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 | + | ||
| 46 | 82 | assert.strictEqual(sp[Symbol.iterator], sp.entries); | |
| 47 | 83 | ||
| 48 | 84 | let key, val; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,20 +11,33 @@ const assert = require('assert'); | |||
| 11 | 11 | ].forEach((i) => { | |
| 12 | 12 | assert.throws(() => Reflect.apply(URL.prototype[i], [], {}), { | |
| 13 | 13 | name: 'TypeError', | |
| 14 | - message: /Cannot read private member/, | ||
| 14 | + message: /Receiver must be an instance of class/, | ||
| 15 | 15 | }); | |
| 16 | 16 | }); | |
| 17 | 17 | ||
| 18 | 18 | [ | |
| 19 | 19 | 'href', | |
| 20 | + 'search', | ||
| 21 | + ].forEach((i) => { | ||
| 22 | + assert.throws(() => Reflect.get(URL.prototype, i, {}), { | ||
| 23 | + name: 'TypeError', | ||
| 24 | + message: /Receiver must be an instance of class/, | ||
| 25 | + }); | ||
| 26 | + | ||
| 27 | + assert.throws(() => Reflect.set(URL.prototype, i, null, {}), { | ||
| 28 | + name: 'TypeError', | ||
| 29 | + message: /Cannot read private member/, | ||
| 30 | + }); | ||
| 31 | + }); | ||
| 32 | + | ||
| 33 | + [ | ||
| 20 | 34 | 'protocol', | |
| 21 | 35 | 'username', | |
| 22 | 36 | 'password', | |
| 23 | 37 | 'host', | |
| 24 | 38 | 'hostname', | |
| 25 | 39 | 'port', | |
| 26 | 40 | 'pathname', | |
| 27 | - 'search', | ||
| 28 | 41 | 'hash', | |
| 29 | 42 | ].forEach((i) => { | |
| 30 | 43 | assert.throws(() => Reflect.get(URL.prototype, i, {}), { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments