Skip to content

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

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

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 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.

Also accessing one single query parameter is tracked independently from the rest of the url. This means that accessing `event.url.searchParams.get("query")` inside a `load` function will make that load function rerun only when the `query` search param changes. For example navigating from `/search?query=svelte&page=1` to `/search?query=svelte&page=2` will not rerun a load function that access `event.url.searchParams.get("query")` but not `event.url.searchParams.get("page")`.

### 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`
- 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)
})
}),

fineGrainedSearchParamsInvalidation: boolean(false)
})
},
true
Expand Down
6 changes: 5 additions & 1 deletion packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { runtime_directory } from '../utils.js';
import { isSvelte5Plus, write_if_changed } from './utils.js';
import colors from 'kleur';

// TODO remove fine_grained_search_params_invalidation after 2.0

/**
* @param {{
* hooks: string | null;
Expand All @@ -24,7 +26,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 +36,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
5 changes: 5 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,11 @@ export interface KitConfig {
*/
pollInterval?: number;
};
/**
* Wether access the searchParams on the url object should track the whole URL or just that specific searchParam
* @default false
*/
fineGrainedSearchParamsInvalidation?: boolean;
}

/**
Expand Down
81 changes: 71 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,11 @@ 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

// TODO remove fine_grained_search_params_invalidation after 2.0
const routes = parse(app);

const default_layout_loader = app.nodes[0];
Expand Down Expand Up @@ -443,7 +445,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 +481,20 @@ 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) => {
// TODO remove fine_grained_search_params_invalidation after 2.0
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 +590,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 +610,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 +636,26 @@ 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);
for (const [key, value] of old_url?.searchParams?.entries?.() ?? []) {
if (new_search_params.get(key) !== value) {
changed.add(key);
}
new_search_params.delete(key);
}
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 +677,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 +688,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 +738,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 +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
6 changes: 4 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,18 @@ 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) {
// TODO remove fine_grained_search_params_invalidation after 2.0
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
4 changes: 3 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,9 @@ 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,
// TODO remove fine_grained_search_params_invalidation after 2.0
fine_grained_search_params_invalidation: options.fine_grained_search_params_invalidation
});
} catch (e) {
load_error = /** @type {Error} */ (e);
Expand Down
39 changes: 29 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,35 @@ 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`
);
}
// TODO remove fine_grained_search_params_invalidation after 2.0
if (fine_grained_search_params_invalidation) {
uses.search_params.add(search_params);
} else {
uses.url = true;
}
}
);

if (state.prerendering) {
disable_search(url);
Expand Down
3 changes: 2 additions & 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,8 @@ export async function render_response({
${properties.join(',\n\t\t\t\t\t\t')}
};`);

const args = ['app', 'element'];
// TODO remove fine_grained_search_params_invalidation after 2.0
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
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const allowed_page_methods = new Set(['GET', 'HEAD', 'OPTIONS']);
* @returns {Promise<Response>}
*/
export async function respond(request, options, manifest, state) {
console.log({ options });
/** URL but stripped from the potential `/__data.json` suffix and its search param */
const url = new URL(request.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
Loading