Skip to content

feat: fine grained search param invalidation #11258

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

Merged
merged 40 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
364c2a4
feat: allow for fine grained invalidation of search params
paoloricciuti Nov 17, 2023
988bcb6
add other instances of search params functions and fix bindings
paoloricciuti Nov 17, 2023
0e51f81
fix tests for make_trackable
paoloricciuti Nov 17, 2023
47b7b9b
old and new url could be null or undefined
paoloricciuti Nov 17, 2023
ac11e75
update test
paoloricciuti Nov 17, 2023
d3b4d2f
try with logs to debug tests
paoloricciuti Nov 17, 2023
e0c67f8
remove only
paoloricciuti Nov 17, 2023
fe6b814
more logs
paoloricciuti Nov 17, 2023
4968f42
access searchParams before adding the getter and remove logs
paoloricciuti Nov 17, 2023
5373dfa
add e2e tests for fine grained invalidation
paoloricciuti Nov 17, 2023
ad8d2c6
fix linting
paoloricciuti Nov 17, 2023
bbc62f1
Add docs
paoloricciuti Nov 17, 2023
5b6b253
update docs
paoloricciuti Nov 20, 2023
d3a9ae8
add feature flag to avoid breaking change
paoloricciuti Nov 21, 2023
8fc1328
Create strange-eyes-sort.md
paoloricciuti Nov 21, 2023
d470608
fix config tests and typescript
paoloricciuti Nov 21, 2023
9e04dad
Update packages/kit/src/exports/public.d.ts
paoloricciuti Nov 23, 2023
92c45f2
Update packages/kit/src/utils/url.spec.js
paoloricciuti Nov 23, 2023
e8bb9f1
Update packages/kit/src/runtime/server/respond.js
paoloricciuti Nov 23, 2023
f4975dd
Update documentation/docs/20-core-concepts/20-load.md
paoloricciuti Nov 23, 2023
6172001
Update documentation/docs/20-core-concepts/20-load.md
paoloricciuti Nov 23, 2023
51c20ab
remove todos and function check
paoloricciuti Nov 23, 2023
23ea3f5
update check_search_params_changed logic to handle multiple search pa…
paoloricciuti Nov 23, 2023
56b50d5
specify where to set fineGrainedSearchParamsInvalidation
paoloricciuti Nov 30, 2023
d72cac9
use vite define to access fine grained config
paoloricciuti Nov 30, 2023
ff8b4bb
remove config since will be on the 2.0 milestone
paoloricciuti Dec 1, 2023
1335515
Merge branch 'fine-grained-search-params' of github.com:paoloricciuti…
Rich-Harris Dec 11, 2023
489db06
more conservative warning
Rich-Harris Dec 11, 2023
0eee019
shrink code slightly
Rich-Harris Dec 11, 2023
e17d328
only create tracked searchParams once
Rich-Harris Dec 11, 2023
4500b93
this feels more readable
Rich-Harris Dec 11, 2023
ea33156
simplify
Rich-Harris Dec 11, 2023
cc9b366
simplify
Rich-Harris Dec 11, 2023
3531b9e
fewer tests
Rich-Harris Dec 11, 2023
5e57b6b
ugh prettier
Rich-Harris Dec 11, 2023
7e9a8ca
tweak docs
Rich-Harris Dec 11, 2023
2aa6d7c
tweak
Rich-Harris Dec 11, 2023
752db32
tweak
Rich-Harris Dec 11, 2023
b766be8
Merge branch 'version-2' into fine-grained-search-params
Rich-Harris Dec 11, 2023
2121520
oops, backwards
Rich-Harris Dec 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strange-eyes-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sveltejs/kit": minor
---

feat: allow for fine grained invalidation of search params
3 changes: 3 additions & 0 deletions documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ A `load` function that calls `await parent()` will also rerun if a parent `load`

Dependency tracking does not apply _after_ the `load` function has returned — for example, accessing `params.x` inside a nested [promise](#streaming-with-promises) will not cause the function to rerun when `params.x` changes. (Don't worry, you'll get a warning in development if you accidentally do this.) Instead, access the parameter in the main body of your `load` function.

Search parameters are tracked independently from the rest of the url. For example, accessing `event.url.searchParams.get("x")` inside a `load` function will make that `load` function re-run when navigating from `?x=1` to `?x=2`, but not when navigating from `?x=1&y=1` to `?x=1&y=2`.

### Manual invalidation

You can also rerun `load` functions that apply to the current page using [`invalidate(url)`](modules#$app-navigation-invalidate), which reruns all `load` functions that depend on `url`, and [`invalidateAll()`](modules#$app-navigation-invalidateall), which reruns every `load` function. Server load functions will never automatically depend on a fetched `url` to avoid leaking secrets to the client.
Expand Down Expand Up @@ -614,6 +616,7 @@ To summarize, a `load` function will rerun in the following situations:

- It references a property of `params` whose value has changed
- It references a property of `url` (such as `url.pathname` or `url.search`) whose value has changed. Properties in `request.url` are _not_ tracked
- It calls `url.searchParams.get(...)`, `url.searchParams.getAll(...)` or `url.searchParams.has(...)` and the parameter in question changes. Accessing other properties of `url.searchParams` will have the same effect as accessing `url.search`.
- It calls `await parent()` and a parent `load` function reran
- It declared a dependency on a specific URL via [`fetch`](#making-fetch-requests) (universal load only) or [`depends`](types#public-types-loadevent), and that URL was marked invalid with [`invalidate(url)`](modules#$app-navigation-invalidate)
- All active `load` functions were forcibly rerun with [`invalidateAll()`](modules#$app-navigation-invalidateall)
Expand Down
73 changes: 64 additions & 9 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,8 @@ export function create_client(app, target) {
params: new Set(),
parent: false,
route: false,
url: false
url: false,
search_params: new Set()
};

const node = await loader();
Expand Down Expand Up @@ -473,9 +474,11 @@ export function create_client(app, target) {
}
}),
data: server_data_node?.data ?? null,
url: make_trackable(url, () => {
uses.url = true;
}),
url: make_trackable(
url,
() => (uses.url = true),
(param) => uses.search_params.add(param)
),
async fetch(resource, init) {
/** @type {URL | string} */
let requested;
Expand Down Expand Up @@ -570,10 +573,18 @@ export function create_client(app, target) {
* @param {boolean} parent_changed
* @param {boolean} route_changed
* @param {boolean} url_changed
* @param {Set<string>} search_params_changed
* @param {import('types').Uses | undefined} uses
* @param {Record<string, string>} params
*/
function has_changed(parent_changed, route_changed, url_changed, uses, params) {
function has_changed(
parent_changed,
route_changed,
url_changed,
search_params_changed,
uses,
params
) {
if (force_invalidation) return true;

if (!uses) return false;
Expand All @@ -582,6 +593,10 @@ export function create_client(app, target) {
if (uses.route && route_changed) return true;
if (uses.url && url_changed) return true;

for (const tracked_params of uses.search_params) {
if (search_params_changed.has(tracked_params)) return true;
}

for (const param of uses.params) {
if (params[param] !== current.params[param]) return true;
}
Expand All @@ -604,6 +619,31 @@ export function create_client(app, target) {
return null;
}

/**
*
* @param {URL | null} old_url
* @param {URL} new_url
*/
function diff_search_params(old_url, new_url) {
if (!old_url) return new Set(new_url.searchParams.keys());

const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]);

for (const key of changed) {
const old_values = old_url.searchParams.getAll(key);
const new_values = new_url.searchParams.getAll(key);

if (
old_values.every((value) => new_values.includes(value)) &&
new_values.every((value) => old_values.includes(value))
) {
changed.delete(key);
}
}

return changed;
}

/**
* @param {import('./types.js').NavigationIntent} intent
* @returns {Promise<import('./types.js').NavigationResult>}
Expand All @@ -625,9 +665,9 @@ export function create_client(app, target) {

/** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
let server_data = null;

const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
const route_changed = current.route ? route.id !== current.route.id : false;
const search_params_changed = diff_search_params(current.url, url);

let parent_invalid = false;
const invalid_server_nodes = loaders.map((loader, i) => {
Expand All @@ -636,7 +676,14 @@ export function create_client(app, target) {
const invalid =
!!loader?.[0] &&
(previous?.loader !== loader[1] ||
has_changed(parent_invalid, route_changed, url_changed, previous.server?.uses, params));
has_changed(
parent_invalid,
route_changed,
url_changed,
search_params_changed,
previous.server?.uses,
params
));

if (invalid) {
// For the next one
Expand Down Expand Up @@ -679,7 +726,14 @@ export function create_client(app, target) {
const valid =
(!server_data_node || server_data_node.type === 'skip') &&
loader[1] === previous?.loader &&
!has_changed(parent_changed, route_changed, url_changed, previous.universal?.uses, params);
!has_changed(
parent_changed,
route_changed,
url_changed,
search_params_changed,
previous.universal?.uses,
params
);
if (valid) return previous;

parent_changed = true;
Expand Down Expand Up @@ -1960,7 +2014,8 @@ function deserialize_uses(uses) {
params: new Set(uses?.params ?? []),
parent: !!uses?.parent,
route: !!uses?.route,
url: !!uses?.url
url: !!uses?.url,
search_params: new Set(uses?.search_params ?? [])
};
}

Expand Down
31 changes: 22 additions & 9 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,31 @@ export async function load_server_data({ event, state, node, parent }) {
params: new Set(),
parent: false,
route: false,
url: false
url: false,
search_params: new Set()
};

const url = make_trackable(event.url, () => {
if (DEV && done && !uses.url) {
console.warn(
`${node.server_id}: Accessing URL properties in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the URL changes`
);
}
const url = make_trackable(
event.url,
() => {
if (DEV && done && !uses.url) {
console.warn(
`${node.server_id}: Accessing URL properties in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the URL changes`
);
}

uses.url = true;
});
uses.url = true;
},
(param) => {
if (DEV && done && !uses.search_params.has(param)) {
console.warn(
`${node.server_id}: Accessing URL properties in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the URL changes`
);
}

uses.search_params.add(param);
}
);

if (state.prerendering) {
disable_search(url);
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/runtime/server/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export function stringify_uses(node) {
uses.push(`"dependencies":${JSON.stringify(Array.from(node.uses.dependencies))}`);
}

if (node.uses && node.uses.search_params.size > 0) {
uses.push(`"search_params":${JSON.stringify(Array.from(node.uses.search_params))}`);
}

if (node.uses && node.uses.params.size > 0) {
uses.push(`"params":${JSON.stringify(Array.from(node.uses.params))}`);
}
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export interface Uses {
parent: boolean;
route: boolean;
url: boolean;
search_params: Set<string>;
}

export type ValidatedConfig = RecursiveRequired<Config>;
Expand Down
26 changes: 24 additions & 2 deletions packages/kit/src/utils/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,40 @@ const tracked_url_properties = /** @type {const} */ ([
'href',
'pathname',
'search',
'searchParams',
'toString',
'toJSON'
]);

/**
* @param {URL} url
* @param {() => void} callback
* @param {(search_param: string) => void} search_params_callback
*/
export function make_trackable(url, callback) {
export function make_trackable(url, callback, search_params_callback) {
const tracked = new URL(url);

Object.defineProperty(tracked, 'searchParams', {
value: new Proxy(tracked.searchParams, {
get(obj, key) {
if (key === 'get' || key === 'getAll' || key === 'has') {
return (/**@type {string}*/ param) => {
search_params_callback(param);
return obj[key](param);
};
}

// if they try to access something different from what is in `tracked_search_params_properties`
// we track the whole url (entries, values, keys etc)
callback();

const value = Reflect.get(obj, key);
return typeof value === 'function' ? value.bind(obj) : value;
}
}),
enumerable: true,
configurable: true
});

for (const property of tracked_url_properties) {
Object.defineProperty(tracked, property, {
get() {
Expand Down
48 changes: 42 additions & 6 deletions packages/kit/src/utils/url.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,62 @@ describe('normalize_path', (test) => {
describe('make_trackable', (test) => {
test('makes URL properties trackable', () => {
let tracked = false;

const url = make_trackable(new URL('https://kit.svelte.dev/docs'), () => {
tracked = true;
});
const url = make_trackable(
new URL('https://kit.svelte.dev/docs'),
() => {
tracked = true;
},
() => {}
);

url.origin;
assert.isNotOk(tracked);
assert.ok(!tracked);

url.pathname;
assert.ok(tracked);
});

test('throws an error when its hash property is accessed', () => {
const url = make_trackable(new URL('https://kit.svelte.dev/docs'), () => {});
const url = make_trackable(
new URL('https://kit.svelte.dev/docs'),
() => {},
() => {}
);

assert.throws(
() => url.hash,
/Cannot access event.url.hash. Consider using `\$page.url.hash` inside a component instead/
);
});

test('track each search param separately if accessed directly', () => {
let tracked = false;
const tracked_search_params = new Set();
const url = make_trackable(
new URL('https://kit.svelte.dev/docs'),
() => {
tracked = true;
},
(search_param) => {
tracked_search_params.add(search_param);
}
);

url.searchParams.get('test');
assert.ok(!tracked);
assert.ok(tracked_search_params.has('test'));

url.searchParams.getAll('test-getall');
assert.ok(!tracked);
assert.ok(tracked_search_params.has('test-getall'));

url.searchParams.has('test-has');
assert.ok(!tracked);
assert.ok(tracked_search_params.has('test-has'));

url.searchParams.entries();
assert.ok(tracked);
});
});

describe('disable_search', (test) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
let count = 0;

export function load({ url }) {
url.searchParams.get('a');
return {
count: count++
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
export let data;
</script>

<span>count: {data.count}</span>

<a data-id="tracked" href="?a={data.count + 1}">Change tracked parameter</a>
<a data-id="untracked" href="?a={data.count}&b={data.count + 1}">Change untracked parameter</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
let count = 0;

export function load({ url }) {
url.searchParams.get('a');
return {
count: count++
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
export let data;
</script>

<span>count: {data.count}</span>

<a data-id="tracked" href="?a={data.count + 1}">Change tracked parameter</a>
<a data-id="untracked" href="?a={data.count}&b={data.count + 1}">Change untracked parameter</a>
Loading