-
-
Notifications
You must be signed in to change notification settings - Fork 40
fix(url): resync cached searchParams when search/href changes #422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| const { | ||
| ArrayPrototypePush, | ||
| FunctionPrototypeCall, | ||
| Map, | ||
| MapPrototypeDelete, | ||
| MapPrototypeGet, | ||
|
|
@@ -39,35 +41,89 @@ InternalAccessor.getData = function (url) { | |
| return MapPrototypeGet(BLOB_STORE, url); | ||
| }; | ||
| URL.InternalAccessor = InternalAccessor; | ||
|
|
||
| // Pushes the params object's serialization onto its URL and records it as the | ||
| // query string the params object is in sync with. | ||
| function writeBack(params) { | ||
| const url = params._url; | ||
| url.search = params.toString(); | ||
| url._searchParamsSource = url.search; | ||
| } | ||
|
|
||
| // Brings `params` back in line with its URL's current query string by mutating | ||
| // it in place: url.searchParams is a same-object accessor, so the one params | ||
| // object handed out for a URL has to survive every reparse of that URL. Only | ||
| // the raw methods captured at construction are used here — the public ones are | ||
| // wrapped to write back into the URL, which would clobber the very string being | ||
| // applied. | ||
| function resync(params) { | ||
| const url = params._url; | ||
| const search = url.search; | ||
| if (url._searchParamsSource === search) { | ||
| return; | ||
| } | ||
| const names = []; | ||
| FunctionPrototypeCall(params._forEach, params, function (value, name) { | ||
| ArrayPrototypePush(names, name); | ||
| }); | ||
| for (let i = 0; i < names.length; i++) { | ||
| FunctionPrototypeCall(params._delete, params, names[i]); | ||
| } | ||
| const parsed = new URLSearchParamsCtor(search); | ||
| FunctionPrototypeCall(params._forEach, parsed, function (value, name) { | ||
| FunctionPrototypeCall(params._append, params, name, value); | ||
| }); | ||
| url._searchParamsSource = search; | ||
| } | ||
|
|
||
| ObjectDefineProperty(URL.prototype, 'searchParams', { | ||
| get() { | ||
| if (this._searchParams == null) { | ||
| this._searchParams = new URLSearchParamsCtor(this.search); | ||
| ObjectDefineProperty(this._searchParams, '_url', { | ||
| const params = new URLSearchParamsCtor(this.search); | ||
| ObjectDefineProperty(this, '_searchParams', { | ||
| enumerable: false, | ||
| writable: true, | ||
| configurable: true, | ||
| value: params, | ||
| }); | ||
| ObjectDefineProperty(this, '_searchParamsSource', { | ||
| enumerable: false, | ||
| writable: true, | ||
| configurable: true, | ||
| value: this.search, | ||
| }); | ||
| ObjectDefineProperty(params, '_url', { | ||
| enumerable: false, | ||
| writable: false, | ||
| value: this, | ||
| }); | ||
| this._searchParams._append = this._searchParams.append; | ||
| this._searchParams.append = function (name, value) { | ||
| params._forEach = params.forEach; | ||
| params._append = params.append; | ||
| params.append = function (name, value) { | ||
| resync(this); | ||
| this._append(name, value); | ||
| this._url.search = this.toString(); | ||
| writeBack(this); | ||
| }; | ||
| this._searchParams._delete = this._searchParams.delete; | ||
| this._searchParams.delete = function (name) { | ||
| params._delete = params.delete; | ||
| params.delete = function (name) { | ||
| resync(this); | ||
| this._delete(name); | ||
| this._url.search = this.toString(); | ||
| writeBack(this); | ||
| }; | ||
| this._searchParams._set = this._searchParams.set; | ||
| this._searchParams.set = function (name, value) { | ||
| params._set = params.set; | ||
| params.set = function (name, value) { | ||
| resync(this); | ||
| this._set(name, value); | ||
| this._url.search = this.toString(); | ||
| writeBack(this); | ||
| }; | ||
| this._searchParams._sort = this._searchParams.sort; | ||
| this._searchParams.sort = function () { | ||
| params._sort = params.sort; | ||
| params.sort = function () { | ||
| resync(this); | ||
| this._sort(); | ||
| this._url.search = this.toString(); | ||
| writeBack(this); | ||
| }; | ||
| } else { | ||
| resync(this._searchParams); | ||
| } | ||
| return this._searchParams; | ||
|
Comment on lines
79
to
128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== repo files matching blob-url/tests =="
git ls-files | rg '(^|/)blob-url\.js$|UrlSearchParamsTests\.js$|blob|searchParams|URLSearchParams' || true
echo
echo "== blob-url outline/size =="
if [ -f NativeScript/runtime/js/blob-url.js ]; then
wc -l NativeScript/runtime/js/blob-url.js
ast-grep outline NativeScript/runtime/js/blob-url.js --view compact || true
echo "== relevant blob-url lines =="
sed -n '1,180p' NativeScript/runtime/js/blob-url.js | cat -n
fi
echo
echo "== UrlSearchParamsTests relevant sections =="
if [ -f NativeScript/runtime/js/UrlSearchParamsTests.js ]; then
wc -l NativeScript/runtime/js/UrlSearchParamsTests.js
git diff -- NativeScript/runtime/js/UrlSearchParamsTests.js | sed -n '1,240p'
rg -n "searchParams|search|href|append|get\\(|getAll\\(|has\\(|keys\\(|values\\(|entries\\(|forEach\\(|toString\\(|sort\\(|Symbol|iterator|size" NativeScript/runtime/js/UrlSearchParamsTests.js NativeScript/runtime/js/blob-url.js || true
fiRepository: NativeScript/ios Length of output: 6055 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== test file relevant lines =="
wc -l TestRunner/app/tests/UrlSearchParamsTests.js
sed -n '1,260p' TestRunner/app/tests/UrlSearchParamsTests.js | cat -n
echo
echo "== URLSearchParams.js test list around searchParams =="
awk '
/URLSearchParams|searchParams|function \(title\)/{start=NR}
NR>=start && NR<=start+220 || NR>=1 && NR<=220 {print NR": "$0}
' TestRunner/app/tests/URLSearchParams.js | cat -n
echo
echo "== static probe for wrapped and unwrapped URLSearchParams methods in blob-url.js =="
python3 - <<'PY'
from pathlib import Path
p=Path("NativeScript/runtime/js/blob-url.js")
s=p.read_text()
start=s.index("ObjectDefineProperty(URL.prototype, 'searchParams', {")
wrap=s.index("params.append = function", start)
end=None
try:
end=s.index("});\n);", wrap)
except ValueError:
end=s.find("});", wrap + 1)
if wrap==-1:
raise SystemExit("wrapper section not found")
section=s[start:end]
unwrapped_methods="get getAll has keys values entries forEach toString size".split()
for name in unwrapped_methods:
needle=f"params.{name}"
if name != "size":
needle=f"{name}:"
print(f"{name}: wrapper_assign={'params._'+name in section and 'params.'+name+' = function' in section} wrapper_calls_resync={'params._'+name+" in section and "resync(this)\n return this._"+name in section)
print("size" in section)
PYRepository: NativeScript/ios Length of output: 35078 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate and inspect URLSearchParams implementation =="
fd -a 'URLSearchParamsImpl\.(h|cpp)$' . | while read -r f; do
echo "--- $f ---"
wc -l "$f"
sed -n '1,260p' "$f" | cat -n
done
echo
echo "== check whether searchParams tests cover direct method stale path =="
python3 - <<'PY'
from pathlib import Path
p = Path("TestRunner/app/tests/UrlSearchParamsTests.js")
s = p.read_text()
cases = s.split("it(")
held = []
stale_read_after_url_change = []
for i, case in enumerate(cases[1:], 1):
# crude brace/expect boundary
code = case[:case.find(";)")]
if "url.searchParams" in code and "url.search" in code:
all_sp = code.count("url.searchParams")
else:
all_sp = 0
if "const sp = url.searchParams" in code and "url.search = " in code:
held.append((i, "has held sp after search reassign"))
# Check direct read methods on sp after url.search reassign without reading url.searchParams immediately.
after_assign = code.index("url.search = ")
segment = code[after_assign:]
direct_sp_reads = ["sp get", "sp getAll", "sp has", "sp keys", "sp values", "sp entries", "sp forEach", "sp toString", "sp [Symbol", "sp.iterator", "sp size"]
if "sp.set" in segment or "sp.append" in segment or "sp.delete" in segment or "sp.sort" in segment:
stale_read_after_url_change.append((i, "mutator on sp before direct read"))
else:
found = []
for term in direct_sp_reads:
if term in segment:
found.append(term)
stale_read_after_url_change.append((i, f"has held sp after search reassign; direct reads not present: {found or 'none'}"))
print("held-after-search tests:", len(held))
print("held-after-search expectations involving sp (not url.searchParams):")
for idx, text in held:
print(f" {idx}: {text}")
print("held-after-search stale direct-read status:")
for idx, text in stale_read_after_url_change:
print(f" {idx}: {text}")
PY
echo
echo "== deterministic JS semantics probe for current wrapper coverage =="
node - <<'JS'
const methods = ["get","getAll","has","keys","values","entries","forEach","toString","size"];
const source = require('fs').readFileSync('NativeScript/runtime/js/blob-url.js','utf8');
const section = source.substring(600); // after relevant constants
for (const name of methods) {
const pattern = new RegExp(`params\\.${name}\\s*=\\s*function`)
console.log(name, "has wrapper", pattern.test(source), section.includes(`resync(this)`), section.includes(`return this._${name}`), section.includes(`${name}:`));
}
JSRepository: NativeScript/ios Length of output: 16590 Make
🤖 Prompt for AI Agents |
||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| describe("URL.searchParams caching", function () { | ||
| it("returns the same object across reads", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| expect(url.searchParams).toBe(url.searchParams); | ||
| }); | ||
|
|
||
| it("parses the query string on the first read", function () { | ||
| const url = new URL("https://example.com/?a=1&b=2&b=3"); | ||
| const sp = url.searchParams; | ||
| expect(sp.get("a")).toBe("1"); | ||
| expect(sp.getAll("b")).toEqual(["2", "3"]); | ||
| expect(sp.size).toBe(3); | ||
| }); | ||
|
|
||
| it("keeps the same object after assigning to search", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
| url.search = "?x=1"; | ||
| expect(url.searchParams).toBe(sp); | ||
| }); | ||
|
|
||
| it("refreshes after assigning to search", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
| expect(sp.get("a")).toBe("1"); | ||
|
|
||
| url.search = "?b=2&c=3"; | ||
|
|
||
| expect(url.searchParams.get("a")).toBe(null); | ||
| expect(url.searchParams.get("b")).toBe("2"); | ||
| expect(url.searchParams.get("c")).toBe("3"); | ||
| expect(url.searchParams.size).toBe(2); | ||
| expect(sp.get("b")).toBe("2"); | ||
| }); | ||
|
|
||
| it("refreshes after assigning to href", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
| expect(sp.get("a")).toBe("1"); | ||
|
|
||
| url.href = "https://example.com/other?q=hello&lang=en"; | ||
|
|
||
| expect(url.searchParams).toBe(sp); | ||
| expect(url.searchParams.get("a")).toBe(null); | ||
| expect(url.searchParams.get("q")).toBe("hello"); | ||
| expect(url.searchParams.get("lang")).toBe("en"); | ||
| }); | ||
|
|
||
| it("refreshes to empty when the query is cleared", function () { | ||
| const url = new URL("https://example.com/?a=1&b=2"); | ||
| const sp = url.searchParams; | ||
| expect(sp.size).toBe(2); | ||
|
|
||
| url.search = ""; | ||
|
|
||
| expect(url.searchParams).toBe(sp); | ||
| expect(url.searchParams.size).toBe(0); | ||
| expect(url.searchParams.toString()).toBe(""); | ||
| }); | ||
|
|
||
| it("preserves duplicate keys when refreshing", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
|
|
||
| url.search = "?a=1&a=2&a=3"; | ||
|
|
||
| expect(url.searchParams.getAll("a")).toEqual(["1", "2", "3"]); | ||
| expect(sp.getAll("a")).toEqual(["1", "2", "3"]); | ||
| }); | ||
|
|
||
| it("writes mutations back to the url", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
|
|
||
| sp.set("a", "9"); | ||
| expect(url.search).toBe("?a=9"); | ||
|
|
||
| sp.append("b", "2"); | ||
| expect(url.search).toBe("?a=9&b=2"); | ||
| expect(url.href).toBe("https://example.com/?a=9&b=2"); | ||
|
|
||
| sp.delete("a"); | ||
| expect(url.search).toBe("?b=2"); | ||
|
|
||
| sp.append("a", "0"); | ||
| sp.sort(); | ||
| expect(url.search).toBe("?a=0&b=2"); | ||
| }); | ||
|
|
||
| it("does not drop state on the read following a mutation", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
|
|
||
| sp.append("b", "2"); | ||
| sp.append("b", "3"); | ||
|
|
||
| const spAfter = url.searchParams; | ||
| expect(spAfter).toBe(sp); | ||
| expect(spAfter.getAll("b")).toEqual(["2", "3"]); | ||
| expect(spAfter.get("a")).toBe("1"); | ||
| expect(spAfter.size).toBe(3); | ||
| }); | ||
|
|
||
| it("interleaves mutations and direct assignments", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
|
|
||
| sp.set("a", "2"); | ||
| url.search = "?b=1"; | ||
|
|
||
| expect(url.searchParams.get("a")).toBe(null); | ||
| expect(url.searchParams.get("b")).toBe("1"); | ||
|
|
||
| sp.set("c", "3"); | ||
| expect(url.search).toBe("?b=1&c=3"); | ||
| expect(url.searchParams.get("c")).toBe("3"); | ||
| }); | ||
|
|
||
| it("does not clobber a reassigned query when mutating a held reference", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| const sp = url.searchParams; | ||
|
|
||
| url.search = "?b=1"; | ||
| sp.set("c", "3"); | ||
|
|
||
| expect(url.search).toBe("?b=1&c=3"); | ||
| expect(sp.get("a")).toBe(null); | ||
| }); | ||
|
|
||
| it("does not expose its cache as an enumerable property", function () { | ||
| const url = new URL("https://example.com/?a=1"); | ||
| UNUSED(url.searchParams); | ||
| url.search = "?b=2"; | ||
| UNUSED(url.searchParams); | ||
|
|
||
| const keys = Object.keys(url); | ||
| expect(keys.indexOf("_searchParams")).toBe(-1); | ||
| expect(keys.indexOf("_searchParamsSource")).toBe(-1); | ||
|
|
||
| const serialized = JSON.stringify(url); | ||
| expect(serialized.indexOf("_searchParams")).toBe(-1); | ||
| expect(serialized.indexOf("_searchParamsSource")).toBe(-1); | ||
|
|
||
| for (const key in url) { | ||
| expect(key).not.toBe("_searchParams"); | ||
| expect(key).not.toBe("_searchParamsSource"); | ||
| } | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: NativeScript/ios
Length of output: 50372
🏁 Script executed:
Repository: NativeScript/ios
Length of output: 860
🏁 Script executed:
Repository: NativeScript/ios
Length of output: 6127
Make the
paramsraw method caches non-enumerable.obj._for = obj.forEach;onurl.searchParamscreates a writable, enumerable own property, soObject.keys(url.searchParams)andfor...inexpose_forEach,_append,_delete,_set, and_sort. UseObjectDefineProperty(params, '_<name>', { enumerable: false, ... })for each cached method, and coversearchParamsenumeration in the cache test with_searchParams/_searchParamsSource.🤖 Prompt for AI Agents