breaking: rework client-driven refreshes#15562
Conversation
🦋 Changeset detectedLatest commit: 0146ebe The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
I hope I am misunderstanding this, but does this mean we will no longer be able to do something like: <script lang="ts">
import { myQuery, myCommand } from "$lib/remote-functions/my-fns.remote";
const myQueryInstance = $derived(myQuery());
</script>
{myQueryInstance.current ?? "N/A"}
<button
type="button"
onclick={() => {
void myCommand().updates(myQueryInstance);
}}
>
Do Something
</button>If that's true, it would be a massive loss, especially since we cannot refresh queries by, say, keys rather than arguments on the server. We currently heavily utilise client-driven refreshes for that reason.
But didn't they serve different purposes? I personally thought it was intuitive that SvelteKit gave you a choice, letting you pick the solution that worked for you.
Isn't it better to simply document the potential security risk rather than removing the feature?
I have to personally disagree with the claim that server-driven refreshes are easy to use. They are only easy to use for very basic use cases, but once you get to more advanced stuff, they get very annoying and are difficult to work with. As an example, if I have a query which returns an array of objects and takes in a bunch of filtering arguments and a command that mutates a single object, the only way, as far as I can tell, to update the query via server-driven refreshes is to include the filtering arguments alongside the other arguments in the update command. Which, apart from being wasteful (you are sending a bunch of arguments which basically act like a key), gives you less flexibility and sometimes isn't easily doable. Also, removing client-driven refreshes eliminates the flexibility of, say, passing a bunch of queries as an array to a component that invokes a command that needs to refresh an unknown number of queries. I can think of many more reasons why removing client-driven refreshes is a bad idea for SvelteKit users. I understand Remote Functions are experimental, and I should expect changes like this, but this one is really bad, in my honest opinion. |
|
Would this disable a refresh that is outside of a form? In some cases I use an interval to It doesn't seem like disabling refresh would prevent an attacker as they could still just make the requests themselves. Not sure why they would go through the refresh API. |
That's just not how security stuff works unfortunately. Documentation is appropriate when there's some action the developer can take, like 'always sanitize user-generated content you pass to There's another reason to make all single-flight mutations server-initiated, and it overlaps with the reasons we don't currently have a tag-based revalidation API: it restricts our future design choices. If the client can specify which things need to be refreshed, either by tag or by the Today, that assumption holds true: all remote functions are handled in the same place. If the server receives a Tag-based revalidation is a naive solution to the problem that makes that impossible (alongside other drawbacks like an inability to e.g. right-click on a remote function and find all the places it's getting called from). Client-driven refreshes are essentially an indirect form of tag-based revalidation. By contrast, server-driven refreshes solve that problem, because a command that refreshes a query is required to have imported the query function, meaning they are guaranteed to end up in the same server bundle. (It's quite possible that your response to all that is 'ok but I don't care' which is fine; I'm just explaining why it's potentially important for other people.)
That is one solution, and to be honest I think it's perfectly fine. But there's another, which is to just forgo the single-flight part. You can just
No, not at all. |
I honestly did not think about that initially, but I see the potential downsides now.
Haha, no. That would be rude. 😅 I am thankful that you took the time to reply with a detailed explanation.
But doesn't this add latency from multiple round-trip requests, rather than everything being done in one go? Unless |
|
Not batched, no, otherwise we're back in the same place where someone could trivially create a massive amount of work via a relatively small payload. But the requests would happen concurrently as soon as the command returned. |
|
OK. I guess I have to say goodbye to this feature then. There are workarounds, but it was nice having such a useful, albeit insecure (as I learned today) feature built in. |
|
What if we exposed the query argument on the instance? <script lang="ts">
import { myQuery, myCommand } from "$lib/remote-functions/my-fns.remote";
const myQueryInstance = $derived(myQuery());
</script>
{myQueryInstance.current ?? "N/A"}
<button
type="button"
onclick={() => {
- void myCommand().updates(myQueryInstance);
+ void myCommand({ updates: myQueryInstance.argument });
}}
>
Do Something
</button>Then on the server, |
I was thinking about suggesting something EXACTLY like that, yes! Anything to make server-driven refreshes easier to work with for queries that take many arguments or for unknown sets of queries would be better than nothing. Especially since the only real thing (at least for me) that makes server-driven refreshes inflexible (compared to client-driven ones) is the need to manually pass query arguments to trigger a refresh. |
|
That seems ok for commands but how about forms? Arbitrarily shaped arguments can't be easily included in a form submission without creating the inputs. Sounds like a lot of work |
|
Don't worry everybody, we've got you taken care of 😉 |
b067d98 to
bcf166d
Compare
(stacked on #15562) Closes #14906 Closes #15551 This sorts object keys when creating the remote function payload, meaning objects with the same key-value pairs will match up as cache keys. As a consequence, this disallows Maps, Sets, and custom objects as remote function parameters, as they're extremely difficult to turn into reliable keys. If you _really_ need a map, set, or custom object, you can serialize it as an object (using something like `Object.fromEntries(map)`) and recreate it on the server. The only other potentially-unexpected result of this is that if you're relying on the order of your object keys across the network boundary, you'll be surprised to find them changed between the remote function invocation and the server handler. If you for some reason _really need_ to depend on your object's key-value order, you should turn it into an array of entries `Object.entries(obj)` and then reconstruct it on the server (`Object.fromEntries(entries)`). --------- Co-authored-by: Rich Harris <richard.a.harris@gmail.com> Co-authored-by: Rich Harris <rich.harris@vercel.com>
|
@abdelfattahradwan just wanted to let you know this has been reworked -- any thoughts on the new API? |
I have been obsessively checking the PR and noticed the changes earlier, hehe. I personally like them, especially now that I can refresh "all" instances of a particular query. I wish there were a version of For example: // Refresh up to 5 queries, whatever they may be.
for (const query of requested(5)) {
query.refresh();
}But I think the current version is good for 80% of my use cases. Thanks a lot for the awesome work! |
We considered some different versions of this. In the first place, the reason While the convenience of the specific API you've outlined is nice, it unfortunately defeats the purpose of the API -- the command must declare which specific queries it is willing to refresh and in what quantities. Reason being, you could have something like this: export const cheap_query = query(...);
export const create_cheap_thing = command('unchecked', () => {
// these things are cheap, and we render a bunch of them -- so we want to be able to refresh a bunch of them!
for (const query of requested(1000)) {
void query.refresh();
}
});
// in an unrelated file
export const very_expensive_query = query(...);
export const create_expensive_thing = command('unchecked', () => {
// these things are expensive -- it is unacceptable to run more than one per command
for (const query of requested(1)) {
void query.refresh();
}
});All someone would have to do is submit a HTTP request to Relatedly but probably less important, this is also important for code splitting reasons -- requiring you to actually import and use the queries you plan on refreshing means that we can prune unused query dependencies when bundling. If a command can arbitrarily refresh any query without importing it, we have to include the code for all queries with every command. |
Ah, I see it now, yeah. Makes sense.
Whoa, I didn't know that was the case. But, as I said before, I personally like the new API and think it will work just fine. It may not be as "convenient" as the old one, but its benefits are definitely welcome. |
Rich-Harris
left a comment
There was a problem hiding this comment.
a few docs notes but otherwise LGTM 👍
|
Can we add a requested(query_1, 5).refresh();
requested(query_2, 5).refresh();which is syntactic sugar to: for (const arg of requested(query_1, 5)) {
void query_1(arg).refresh();
}
for (const arg of requested(query_2, 5)) {
void query_2(arg).refresh();
}Of course you're free to choose which form to use depending on your needs. |
Co-authored-by: Rich Harris <hello@rich-harris.dev>
|
@henrykrinkle01 good idea, added -- we called it |
31c7504
into
main
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @sveltejs/kit@2.56.0 ### Minor Changes - breaking: rework client-driven refreshes ([#15562](#15562)) - breaking: stabilize remote function caching by sorting object keys ([#15570](#15570)) - breaking: add `run()` method to queries, disallow awaiting queries outside render ([#15533](#15533)) - feat: support TypeScript 6.0 ([#15595](#15595)) - breaking: isolate command-triggered query refresh failures per-query ([#15562](#15562)) - feat: use `hydratable` for remote function transport ([#15533](#15533)) - feat: allow `form` fields to specify a default value (`field.as(type, value)`) ([#15577](#15577)) ### Patch Changes - fix: don't request new data when `.refresh` is called on a query with no cache entry ([#15533](#15533)) - fix: allow using multiple remote functions within one async derived ([#15561](#15561)) - fix: avoid false-positive overridden Vite `base` setting warning when setting a `paths.base` in `svelte.config.js` ([#15623](#15623)) - fix: manage queries in their own `$effect.root` ([#15533](#15533)) - fix: avoid `inlineDynamicImports` deprecation warning when building the service worker with Vite 8 ([#15550](#15550)) - fix: correctly escape backticks when precomputing CSS ([#15593](#15593)) - fix: discard obsolete forks before finishing navigation ([#15634](#15634)) - chore: tighten up override implementation ([#15562](#15562)) - fix: ensure the default Svelte 5 `error.svelte` file uses runes mode ([#15609](#15609)) - fix: deduplicate same-cache-key `batch` calls during SSR ([#15533](#15533)) - fix: decrement pending_count when form callback doesn't call submit() ([#15520](#15520)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@sveltejs/kit](https://svelte.dev) ([source](https://github.com/sveltejs/kit/tree/HEAD/packages/kit)) | [`2.55.0` → `2.56.1`](https://renovatebot.com/diffs/npm/@sveltejs%2fkit/2.55.0/2.56.1) |  |  | --- ### Release Notes <details> <summary>sveltejs/kit (@​sveltejs/kit)</summary> ### [`v2.56.1`](https://github.com/sveltejs/kit/blob/HEAD/packages/kit/CHANGELOG.md#2561) [Compare Source](https://github.com/sveltejs/kit/compare/@sveltejs/kit@2.56.0...@sveltejs/kit@2.56.1) ##### Patch Changes - chore: update JSDoc ([#​15640](sveltejs/kit#15640)) ### [`v2.56.0`](https://github.com/sveltejs/kit/blob/HEAD/packages/kit/CHANGELOG.md#2560) [Compare Source](https://github.com/sveltejs/kit/compare/@sveltejs/kit@2.55.0...@sveltejs/kit@2.56.0) ##### Minor Changes - breaking: rework client-driven refreshes ([#​15562](sveltejs/kit#15562)) - breaking: stabilize remote function caching by sorting object keys ([#​15570](sveltejs/kit#15570)) - breaking: add `run()` method to queries, disallow awaiting queries outside render ([#​15533](sveltejs/kit#15533)) - feat: support TypeScript 6.0 ([#​15595](sveltejs/kit#15595)) - breaking: isolate command-triggered query refresh failures per-query ([#​15562](sveltejs/kit#15562)) - feat: use `hydratable` for remote function transport ([#​15533](sveltejs/kit#15533)) - feat: allow `form` fields to specify a default value (`field.as(type, value)`) ([#​15577](sveltejs/kit#15577)) ##### Patch Changes - fix: don't request new data when `.refresh` is called on a query with no cache entry ([#​15533](sveltejs/kit#15533)) - fix: allow using multiple remote functions within one async derived ([#​15561](sveltejs/kit#15561)) - fix: avoid false-positive overridden Vite `base` setting warning when setting a `paths.base` in `svelte.config.js` ([#​15623](sveltejs/kit#15623)) - fix: manage queries in their own `$effect.root` ([#​15533](sveltejs/kit#15533)) - fix: avoid `inlineDynamicImports` deprecation warning when building the service worker with Vite 8 ([#​15550](sveltejs/kit#15550)) - fix: correctly escape backticks when precomputing CSS ([#​15593](sveltejs/kit#15593)) - fix: discard obsolete forks before finishing navigation ([#​15634](sveltejs/kit#15634)) - chore: tighten up override implementation ([#​15562](sveltejs/kit#15562)) - fix: ensure the default Svelte 5 `error.svelte` file uses runes mode ([#​15609](sveltejs/kit#15609)) - fix: deduplicate same-cache-key `batch` calls during SSR ([#​15533](sveltejs/kit#15533)) - fix: decrement pending\_count when form callback doesn't call submit() ([#​15520](sveltejs/kit#15520)) </details> --- ### Configuration 📅 **Schedule**: (in timezone UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDQuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwNC4xIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJLaW5kL0RlcGVuZGVuY2llcyJdfQ==--> Reviewed-on: https://codeberg.org/ampmod/aw3/pulls/57 Co-authored-by: AmpMod Bot <ampmod-bot@noreply.codeberg.org> Co-committed-by: AmpMod Bot <ampmod-bot@noreply.codeberg.org>
…t 2.56+ SvelteKit 2.56 reworked client-driven refreshes (sveltejs/kit#15562). command.updates(query.withOverride(...)) is now a client *request* that the server must accept via requested(). Without this, the playground's optimistic update would flash the new value, then revert to the stale cached value once the override expired, because the server never refreshed the query. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Another breaking change to queries.
Prior to this PR, it was possible to write the following on the client:
This would tell the server to run the command and refresh
query1andquery2in one round-trip. This is awesome, but it has a crucial flaw: There's no way to design the API so that it's completely safe. The client can request arbitrarily many refreshes of arbitrarily many functions, all boxed up in one nice HTTP request. This kind of pattern is difficult to control for at any level of the security stack.Our original solution was to remove client-driven refreshes entirely, relying on server-driven refreshes (where, in the server handler, you call
query1(/* args */).refresh()). This works much of the time, but it starts to break down when you don't know exactly what args to callquery1with. For example, if you had a call togetPullRequests({ filter: 'author:elliott-with-the-longest-name-on-github}), it would be hard to know to refresh that query inside thecreatePullRequestcommand, as you wouldn't naturally have any access to the args thatgetPullRequests` was called with on the client.Thinking through the problem, we realized something important: The server always knows which categories of data it can invalidate, but sometimes only the client knows which specific data to invalidate. This made the API decisions pretty easy. You can continue to write the same code as before, but instead of being a client-driven update, it's now a client-requested update. On the server, you need to accept the requested updates:
requestedaccepts a query function (not a query instance) and returns all of the validated args that the client requested for the associated query. Its second argument is required, and is a limit on the number of instances this command is willing to refresh. A couple of notes on its behavior:query.refresh()failed, the entire command would fail. This is no longer the case. Refresh failures are sent back to the client per-query rather than crashing the entire command.There's also a small additional trick we've added to
command.updates-- you can pass in a query function to request all active instances be refreshed:It's perfectly legal to provide
getPullRequestsand a more-specificpullRequestsInstance.withOverride-- the more-specific request will take precedence. It is not legal to request multiple refreshes of the same query with overrides.