Skip to content
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

feat: allow for fine grained invalidation of search params #11066

Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 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
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 @@ -548,6 +548,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.

When setting the `fineGrainedSearchParamsInvalidation` option to `true`, accessing a query parameter is tracked independently from the rest of the url. For example, accessing `event.url.searchParams.get("query")` inside a `load` function will make that `load` function rerun only when the `query` search param changes: Navigating from `/search?query=svelte&page=1` to `/search?query=svelte&page=2` will not rerun the load function.
paoloricciuti marked this conversation as resolved.
Show resolved Hide resolved

### 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 @@ -597,6 +599,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 specific search param passed to those functions changes. Accessing other properties of searchParams will have the same effect as accessing `url.search`. When `fineGrainedSearchParamsInvalidation` is `false`, accessing _any_ search param will cause the load function to rerun.
- 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
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ const get_defaults = (prefix = '') => ({
version: {
name: Date.now().toString(),
pollInterval: 0
}
},
fineGrainedSearchParamsInvalidation: false
}
});

Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ const options = object(
version: object({
name: string(Date.now().toString()),
pollInterval: number(0)
})
}),
// TODO v2: remove this option (always true)
fineGrainedSearchParamsInvalidation: boolean(false)
paoloricciuti marked this conversation as resolved.
Show resolved Hide resolved
})
},
true
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const server_template = ({
runtime_directory,
template,
error_page
}) => `
}) =>
`
import root from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
import { set_building } from '__sveltekit/environment';
import { set_assets } from '__sveltekit/paths';
Expand All @@ -33,6 +34,7 @@ import { set_private_env, set_public_env } from '${runtime_directory}/shared-ser
export const options = {
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
csp: ${s(config.kit.csp)},
fine_grained_search_params_invalidation: ${config.kit.fineGrainedSearchParamsInvalidation},
csrf_check_origin: ${s(config.kit.csrf.checkOrigin)},
track_server_fetches: ${s(config.kit.dangerZone.trackServerFetches)},
embedded: ${config.kit.embedded},
Expand Down
6 changes: 6 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,12 @@ export interface KitConfig {
*/
pollInterval?: number;
};
/**
* When set to `true`, accessing `searchParams` on the `url` object tracks just that specific searchParam and not the whole URL, resulting in less load function reruns.
* This option will be removed and always be `true` in SvelteKit version 2.
* @default false
*/
fineGrainedSearchParamsInvalidation?: boolean;
}

/**
Expand Down
88 changes: 78 additions & 10 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ function native_navigation(url) {
/**
* @param {import('./types.js').SvelteKitApp} app
* @param {HTMLElement} target
* @param {boolean} fine_grained_search_params_invalidation
* @returns {import('./types.js').Client}
*/
export function create_client(app, target) {
export function create_client(app, target, fine_grained_search_params_invalidation) {
Copy link
Contributor

@gtm-nayan gtm-nayan Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a reason to pass this into create_client as a param. This could be done using define and that'd reduce the complexity of the config quite a bit on the server side.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think i've ever heard of define...do you have a link to some resource i can look into?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh i think now i realized it's from vite...exploring this a bit

const routes = parse(app);

const default_layout_loader = app.nodes[0];
Expand Down Expand Up @@ -443,7 +444,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 @@ -478,9 +480,19 @@ 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;
},
(search_param) => {
if (fine_grained_search_params_invalidation) {
uses.search_params.add(search_param);
} else {
uses.url = true;
}
}
),
async fetch(resource, init) {
/** @type {URL | string} */
let requested;
Expand Down Expand Up @@ -576,10 +588,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 @@ -588,6 +608,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 @@ -610,6 +634,35 @@ export function create_client(app, target) {
return null;
}

/**
*
* @param {URL} [old_url]
* @param {URL} [new_url]
*/
function check_search_params_changed(old_url, new_url) {
const changed = new Set();
const new_search_params = new URLSearchParams(new_url?.searchParams);
const old_search_params = old_url?.searchParams;
for (const key of new Set(old_search_params?.keys?.() ?? [])) {
const new_get_all = new_search_params.getAll(key);
const old_get_all = old_search_params?.getAll?.(key) ?? [];
if (
// check if the two arrays contains the same values
!(
new_get_all.every((query_param) => old_get_all.includes(query_param)) &&
old_get_all.every((query_param) => new_get_all.includes(query_param))
)
) {
changed.add(key);
}
new_search_params.delete(key);
paoloricciuti marked this conversation as resolved.
Show resolved Hide resolved
}
for (const [key] of new_search_params) {
changed.add(key);
}
return changed;
}

/**
* @param {import('./types.js').NavigationIntent} intent
* @returns {Promise<import('./types.js').NavigationResult>}
Expand All @@ -631,9 +684,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 = check_search_params_changed(current.url, url);

let parent_invalid = false;
const invalid_server_nodes = loaders.map((loader, i) => {
Expand All @@ -642,7 +695,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 @@ -685,7 +745,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 @@ -1954,7 +2021,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
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/client/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import { init } from './singletons.js';
/**
* @param {import('./types.js').SvelteKitApp} app
* @param {HTMLElement} target
* @param {boolean} fine_grained_search_params_invalidation
* @param {Parameters<import('./types.js').Client['_hydrate']>[0]} [hydrate]
*/
export async function start(app, target, hydrate) {
export async function start(app, target, fine_grained_search_params_invalidation, hydrate) {
if (DEV && target === document.body) {
console.warn(
'Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>'
);
}

const client = create_client(app, target);
const client = create_client(app, target, fine_grained_search_params_invalidation);

init({ client });

Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export async function render_data(
}
return data;
},
track_server_fetches: options.track_server_fetches
track_server_fetches: options.track_server_fetches,
fine_grained_search_params_invalidation: options.fine_grained_search_params_invalidation
});
} catch (e) {
aborted = true;
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ export async function render_page(event, page, options, manifest, state, resolve
}
return data;
},
track_server_fetches: options.track_server_fetches
track_server_fetches: options.track_server_fetches,
fine_grained_search_params_invalidation: options.fine_grained_search_params_invalidation
});
} catch (e) {
load_error = /** @type {Error} */ (e);
Expand Down
38 changes: 28 additions & 10 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { validate_depends } from '../../shared.js';
* node: import('types').SSRNode | undefined;
* parent: () => Promise<Record<string, any>>;
* track_server_fetches: boolean;
* fine_grained_search_params_invalidation: boolean | undefined;
* }} opts
* @returns {Promise<import('types').ServerDataNode | null>}
*/
Expand All @@ -20,7 +21,8 @@ export async function load_server_data({
node,
parent,
// TODO 2.0: Remove this
track_server_fetches
track_server_fetches,
fine_grained_search_params_invalidation
}) {
if (!node?.server) return null;

Expand All @@ -31,18 +33,34 @@ export async function load_server_data({
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;
},
(search_params) => {
if (DEV && done && uses.search_params.size === 0) {
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`
);
}
if (fine_grained_search_params_invalidation) {
uses.search_params.add(search_params);
} else {
uses.url = true;
}
}
);

if (state.prerendering) {
disable_search(url);
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ export async function render_response({
${properties.join(',\n\t\t\t\t\t\t')}
};`);

const args = ['app', 'element'];
const args = ['app', 'element', `${options.fine_grained_search_params_invalidation}`];

blocks.push('const element = document.currentScript.parentElement;');

Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export async function respond_with_error({
state,
node: default_layout,
parent: async () => ({}),
track_server_fetches: options.track_server_fetches
track_server_fetches: options.track_server_fetches,
fine_grained_search_params_invalidation: options.fine_grained_search_params_invalidation
});

const server_data = await server_data_promise;
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
2 changes: 2 additions & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export type SSRNodeLoader = () => Promise<SSRNode>;
export interface SSROptions {
app_template_contains_nonce: boolean;
csp: ValidatedConfig['kit']['csp'];
fine_grained_search_params_invalidation?: boolean;
csrf_check_origin: boolean;
track_server_fetches: boolean;
embedded: boolean;
Expand Down Expand Up @@ -408,6 +409,7 @@ export interface Uses {
parent: boolean;
route: boolean;
url: boolean;
search_params: Set<string>;
}

export type ValidatedConfig = RecursiveRequired<Config>;
Expand Down
Loading
Loading