Remote Functions #13897
Replies: 101 comments 335 replies
-
yet another long- |
Beta Was this translation helpful? Give feedback.
-
The hype is real for this - amazing changes - and validation!!! Omg..
*cough* Zero *cough* 👀 |
Beta Was this translation helpful? Give feedback.
-
I am about to cry 🥹 This is a freaking great feature!. |
Beta Was this translation helpful? Give feedback.
-
Brilliant 🎉 |
Beta Was this translation helpful? Give feedback.
-
I'm excited to see the idea for validation! Have you considered something like this to avoid the extra import and slightly awkward nested functions? export const getStuff = query.validate(schema, async ({ id }) => {
// `id` is typed correctly. if the function
// was called with bad arguments, it will
// result in a 422 response
}); |
Beta Was this translation helpful? Give feedback.
-
wow! |
Beta Was this translation helpful? Give feedback.
-
Wow, this is awesome! It seems so intuitive and satisfying to use! |
Beta Was this translation helpful? Give feedback.
-
sounds great! I have been struggling to implement a fairly complex SvelteKit-app that renders sequences of sensor data from lidars, cameras and radars. each frame in a sequence can be 20-100 mb. at the same time, the sensor data is pretty static so caching can and will be used on as many levels as possible. super interested in what you have planned for caching!
this would be great and it would basically replace what I have implemented on my own to cache requests. it would be great with control over at least max size when used as some LRU-cache. but also max size in terms of size on the disk and maybe also some TTL. and of course some way to manually invalidate the cache per function or globally, if needed.
yeah this would also be useful! I would even go one step further and consider some sort of persistent cache for users - maybe in IndexedDB or something along those lines? otherwise that is something I plan to implement anyways - so I don’t have to stream hundreds of megabytes to a user if they accidentally refresh the page. again one thing less I would have to implement in userland so if this was provided as some opt-in feature by sveltekit it would be the dream. the same level of configuration I brought up for the server caching would be useful in a persistent client side cache as well —- please let me know if I can provide feedback in any more structured way, I would love to test this out in practice later on and help out as much as I can |
Beta Was this translation helpful? Give feedback.
-
This reminds me of the days I was a GWT expert, back in 2010-16. I must say there were disappointments with the RPC calls architecture, but I forgot the use cases. What I remember is that it was frustrating to call them manually, eg. from a mobile app, so I had to isolate then as only callers to service methods (having them like boilerplate code). Maybe the architecture would allow a non proprietary protocol so that it can be implemented in case of need... |
Beta Was this translation helpful? Give feedback.
-
Where can I subscribe so that I get a notification once this is testable? The form part is incredibly valuable <3 |
Beta Was this translation helpful? Give feedback.
-
This is going to be incredible, this and async address all of the features I have been wanting from Svelte. One question for the team, have you considered adding a <script>
import { getLikes, addLike } from './data.remote';
let { item } = $props();
</script>
<addLike.Form enhance={async () => {...}}>
<input type="hidden" name="id" value={item.id} />
<button>add like</button>
</addLike.Form>
<p>likes: {await getLikes(item.id)}</p> |
Beta Was this translation helpful? Give feedback.
-
if |
Beta Was this translation helpful? Give feedback.
-
EDIT: Ignore this. I didn't see it was a template string.
|
Beta Was this translation helpful? Give feedback.
-
While I don't think I will use this many times since I build mostly true SPAs with a separate backend, I really believe these will be very useful for fullstack apps or backend-for-frontend apps. There are two additions I would add: Output validationNot so much due to the validation but due to the validator being able to transform the output (setting default values for I think a tRPC-like signature like this would be more ergonomic if output validation is added but the signature is definitely not a big deal: const my_procedure = rpc
.input(input_schema)
.output(output_schema)
.query(schema, async (input) => {
//body
})
//maybe allow chaining a .mutation here to create a mutation that implicitly invalidates the query? Support for middleware (using the tRPC-like signature above)Middlewares are a great way to manage dependency injection in a declarative manner. const my_procedure = rpc
.input(input_schema)
.output(output_schema)
.middleware(services) //Whatever is returned from a middleware is passed to the next middleware as the first parameter
.middleware(logging)
.middleware(stats)
.middleware(with_cache({storage: 'redis', ttl: 3600})) //The function returned by with_cache() would be accessing a redis service returned by the first middleware
.query(schema, async (input, ctx) => { //ctx holds the awaited return value of the last middleware
//body
}) I also would like to know what are the plans regarding Personally I think |
Beta Was this translation helpful? Give feedback.
-
Why would this ever be needed / isn't this what TypeScript is for? You're already in control of the data you return from the function, so just... don't return data in the wrong shape / type the result of your function. (Am I missing something?)
Is this necessary in a world where you have access to |
Beta Was this translation helpful? Give feedback.
-
I'm looking forward to this feature. Has it been discussed how auth would be used with remote functions? |
Beta Was this translation helpful? Give feedback.
-
Since all these calls will be wrapped in standard fetch requests, do we get a way to manipulate fetch options? Sometimes setting headers might be useful or setting credentials, an abort signal, or the like. |
Beta Was this translation helpful? Give feedback.
-
This is looking really exciting! A couple things that would keep us from being able to use it right now:
We currently use ORPC which does this very well. Ideally I would love it if Svelte remote functions felt like it was sugar over that API. 😄
// Shared config
const pub = apiBase.use(sessionMiddleware);
const authed = pub.use(requireAuthMiddleware);
export const api = {
public: pub,
authed,
};
// RPC function
export const all = api.authed.handler(async () => {
return await db.select().from(users);
});
const changes = api.authed.handler(async function* ({ input, lastEventId }) {
while (true) {
yield { message: 'Hello, world!' }
await new Promise(resolve => setTimeout(resolve, 1000))
}
})
// From a normal RPC:
const query = useQuery(orpc.useres.all.queryOptions())
// From a SSE source (streamed):
const query = useQuery(orpc.streamed.experimental_streamedOptions({
input: { id: 123 },
queryFnOptions: {
refetchMode: 'reset',
maxChunks: 3,
}
}))
Anyways, very excited for this new feature. Nice work! |
Beta Was this translation helpful? Give feedback.
-
I got an question now, how would the +server.ts endpoints function now? Are you guys planning to remove these +server.ts? |
Beta Was this translation helpful? Give feedback.
-
The branches rpc-ssr-2 and async contain the latest updates for async Svelte and the remote functions (command, query, prerender, form). |
Beta Was this translation helpful? Give feedback.
-
Something I have found difficult with the progressive enhancement primitives of SvelteKit is that there's not really a clear path for nested data or arrays. I love the Currently, I rely on Superforms to do the data transformation on the client, but even there it's not progressively enhance-able. This RFC is consuming most of Superforms features, which makes its future tenuous (especially since it still hasn't adopted runes). |
Beta Was this translation helpful? Give feedback.
-
Is it possible to make remote function endpoints have unreadable names and not exported variable name in prod environment? |
Beta Was this translation helpful? Give feedback.
-
Do they also get a error property? If I understand correctly we could then have this, with no need for svelte boundary usage. const todos = getTodos()
...
{#if todos.loading}
loading...
{:else if todos.error}
failed: {todos.error.message}
{:else if todos.current}
{#each todos.current as todo}
{todo.name}
{/each}
{/if} |
Beta Was this translation helpful? Give feedback.
-
after having async svelte, what problems does builtin remote functions solve that third party libraries like orpc doesn't? |
Beta Was this translation helpful? Give feedback.
-
import { query, command, router } from "$app/server"
export const comments = router({
get: query(z.string(), { ... }),
create: command(z.object({ text: z.string() }), ({ text }) => { ... }),
edit: command(z.object({ id: z.string(), text: z.string() }), ({ id, text }) => { ... }),
delete: command(z.string(), (id) => { ... }),
}) This would just help to organize remote.js files with many functions, as well as reduce the number of imports and add autocompletion when calling remote functions from .svelte files (As an alternative you could
If not the size of the serialized input will be limited when using a reverse proxy with Sveltekit (for example nginx's |
Beta Was this translation helpful? Give feedback.
-
Something we use quite a lot of is another loading indication sometimes called Superforms has a really great page about the concept Anyway I'm fairly certain you will wan't people to solve this in userland, I just wanted to suggest it in case you had not thought about it. |
Beta Was this translation helpful? Give feedback.
-
Hello, I want to know when this feature will be released. It doesn't work when I pull the sample. I really need this feature. I am deeply troubled. When I need to connect to multiple backend APIs, I need to add +server.ts first and then add load. The code structure is very messy and there are a lot of boilerplate codes. This feature perfectly solves all the problems without the need for an additional bff layer service. |
Beta Was this translation helpful? Give feedback.
-
We must think of naming convention for this, it's very confusing otherwise: <!--
is `getLikes` a normal async function or a remote query?
if this is normal function, it should be deduped to reduce network traffic.
-->
<span>
{await getLikes(post.id)} likes
</span>
<!-- somewhere else ... -->
<LikeButton
count={await getLikes(post.id}
onclick={() => {
// is `like` just an async function or a remote command?
// if it's a command, it should update the getLikes with `updates` for one-round trip update.
await like(post.id);
await getLikes(id).refresh();
}}
/> |
Beta Was this translation helpful? Give feedback.
-
Would remote functions SSR work with workbox-strategies? |
Beta Was this translation helpful? Give feedback.
-
Will you be able to call the remote functions from the |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
tl;dr
Remote functions are a new concept in SvelteKit that allow you to declare functions inside a
.remote.ts
file, import them inside Svelte components and call them like regular functions. On the server they work like regular functions (and can access environment variables and database clients and so on), while on the client they become wrappers aroundfetch
. If you're familiar with RPC and 'server functions', this is basically our take on the concept, that addresses some of the drawbacks we've encountered with other implementations of the idea.You can try it out soon by installing from the corresponding PR once it's out...
... and add the experimental options
We'll also link an example here, soon.
Background
Today, SvelteKit's data loading is based on the concept of loaders. You declare a
load
function inside a+page/layout(.server).ts
file, fetch the required data for the whole page in it, and retrieve the result via thedata
prop inside the sibling+page/layout.svelte
file.This allows for a very structured approach to data loading and works well for sites where the loaded data is used on the whole page. When that isn't so clear-cut, some drawbacks become apparent:
Additionally, since
load
and the resultingdata
prop are somewhat disconnected, we have to resort to very clever but somewhat weird solutions like generating hidden types you import as./$types
or even using a TypeScript plugin to avoid having to import the types yourself. An approach where we can use TypeScript natively would simplify all this and make it more robust.Lastly, apart from form actions SvelteKit doesn't give you a good way to mutate data. You can use
+server.ts
files and do fetch requests against these endpoints, but it's a lot of ceremony and you lose type safety.Asynchronous Svelte
A couple of weeks ago we introduced Asynchronous Svelte, a proposal to allow using
await
at the top level of Svelte components and inside the template.This in itself is already valuable, but the way SvelteKit's data loading is architected right now you can't take full advantage of it inside SvelteKit.
Requirements
A solution should fix these drawbacks and take advantage of Svelte's capabilities, and specifically should:
An important additional requirement is that modules that can run in the client (including components) must never include code that can only run on the server. A remote function must be able to safely access things like database clients and environment variables that should not (or cannot) be accessed from the client.
In practice, this means that remote functions must be declared in a separate module. Over the last few years various systems have experimented with 'server functions' declared alongside universal/client code, and we're relieved to see a growing consensus that this is a flawed approach that trades security and clarity for a modicum of convenience. You're one innocent mistake away from leaking sensitive information (such as API keys or the shape of your database), and even if tooling successfully treeshakes it away, it may remain in sourcemaps. While no framework can completely prevent you from spilling secrets, we think colocating server and client code makes it much more likely.
Allowing server functions to be declared in arbitrary locations also masks the fact that they are effectively creating a publicly accessible endpoint. Even in systems that prevent server functions from being declared in client code (such as
"use server"
in React Server Components), experienced developers can be caught out. We prefer a design that emphasises the public nature of remote functions rather than the fact that they run on the server, and avoids any confusion around lexical scope.Design
Remote functions are declared inside a
.remote.ts
file. You can import them inside Svelte components and call them like regular async functions. On the server you import them directly; on the client, the module is transformed into a collection of functions that request data from the server.Today we’re introducing four types of remote function:
query
,form
,command
andprerender
.query
Queries are for reading dynamic data from the server. They can have zero or one arguments. If they have an argument, you're encouraged to validate the input via a schema which you can create with libraries like
Zod
(more details in the upcoming Validation section). The argument is serialized with devalue, which handles types likeDate
andMap
in addition to JSON, and takes the transport hook into account.When called during server-rendering, the result is serialized into the HTML payload so that the data isn't requested again during hydration.
Queries are thenable, meaning they can be awaited. But they're not just promises, they also provide properties like
loading
andcurrent
(which contains the most recent value, but is initiallyundefined
) and methods likeoverride(...)
(see the section on optimistic UI, below) andrefresh()
, which fetches new data from the server. We’ll see an example of that in a moment.Query objects are cached in memory for as long as they are actively used, using the serialized arguments as a key — in other words
myQuery(id) === myQuery(id)
. Refreshing or overriding a query will update every occurrence of it on the page. We use Svelte's reactivity system to intelligently clear the cache to avoid memory leaks.form
Forms are the preferred way to write data to the server:
A form object such as
addLike
has enumerable properties —method
,action
andonsubmit
— that can be spread onto a<form>
element. This allows the form to work without JavaScript (i.e. it submits data and reloads the page), but it will also automatically progressively enhance the form, submitting data without reloading the entire page.By default, all queries used on the page (along with any
load
functions) are automatically refreshed following a form submission, meaninggetLikes(...)
will show updated data.In addition to the enumerable properties,
addLike
has non-enumerable properties such asresult
, containing the return value, andenhance
which allows us to customize how the form is progressively enhanced. We can use this to indicate that onlygetLikes(...)
should be refreshed and through that also enable single-flight mutations — meaning that the updated data forgetLikes(...)
is sent back from the server along with the form result. Additionally we provide nicer behaviour in the case that the submission fails (by default, an error page will be shown):Alternatively we can also enable single-flight mutations by adding the
refresh
call to the server, which means all calls toaddLike
will leverage single-flight mutations compared to only those who usesubmit.updates(...)
:import { query, form } from '$app/server'; import * as db from '$lib/server/db'; export const getLikes = query(async (id: string) => { const [row] = await sql`select likes from item where id = ${id}`; return row.likes; }); export const addLike = form(async (data: FormData) => { const id = data.get('id') as string; await sql` update item set likes = likes + 1 where id = ${id} `; + await getLikes(id).refresh(); // we can return arbitrary data from a form function return { success: true }; });
command
For cases where serving no-JS users is impractical or undesirable,
command
offers an alternative way to write data to the server.This time, simply call
addLike
, from (for example) an event handler:As with forms, we can refresh associated queries on the server during the command or via
.updates(...)
on the client for a single-flight mutation, otherwise all queries will automatically be refreshed.prerender
This function is like
query
except that it will be invoked at build time to prerender the result. Use this for data that changes at most once per redeployment.You can use
prerender
functions on pages that are otherwise dynamic, allowing for partial prerendering of your data. This results in very fast navigation, since prerendered data can live on a CDN along with your other static assets.Prerendering is automatic, driven by SvelteKit's crawler, but you can also provide an
entries
option to control what gets prerendered, in case some pages cannot be reached by the crawler:If the function is called at runtime with arguments that were not prerendered it will error by default, as the code will not have been included in the server bundle. You can set
dynamic: true
to change this behaviour:import z from 'zod'; import { prerender } from '$app/server'; export const getBlogPost = prerender( z.string(), (slug) => { // ... }, { + dynamic: true, entries: () => ['first-post', 'second-post', 'third-post'] } );
Optimistic updates
Queries have an
withOverride
method, which is useful for optimistic updates. It receives a function that transforms the query, and must be passed tosubmit().updates(...)
ormyCommand.updates(...)
:Multiple overrides can be applied simultaneously — if you click the button multiple times, the number of likes will increment accordingly. If
addLike()
fails, the override releases and will decrement it again, otherwise the updated data (sans override) will match the optimistic update.Validation
Data validation is an important part of remote functions. They look like regular JavaScript functions but they are actually auto-generated public endpoints. For that reason we strongly encourage you to validate the input using a Standard Schema object, which you create for example through
Zod
:By default a failed schema validation will result in a generic
400
response with just the textBad Request
. You can adjust the returned shape by implementing thehandleValidationError
hook inhooks.server.js
. The returned shape must adhere to the shape ofApp.Error
.If you wish to opt out of validation (for example because you validate through other means, or just know this isn't a problem), you can do so by passing
'unchecked'
as the first argument instead:In case your
query
does not accept arguments you don't need to pass a schema or'unchecked'
- validation is added under the hood on your behalf to check that no arguments are passed to this function:The same applies to
prerender
andcommand
.form
does not accept a schema since you are always passed aFormData
object which you need to parse and validate yourself.Accessing the current request event
SvelteKit exposes a function called
getRequestEvent
which allows you to get details of the current request inside hooks,load
, actions, server endpoints, and the functions they call.This function can now also be used in
query
,form
andcommand
, allowing us to do things like reading and writing cookies:Note that some properties of
RequestEvent
are different in remote functions. There are noparams
orroute.id
, and you cannot set headers (other than writing cookies, and then only insideform
andcommand
functions), andurl.pathname
is always/
(since the path that’s actually being requested by the client is purely an implementation detail).Redirects
Inside
query
,form
andprerender
functions it is possible to use theredirect(...)
function. It is not possible insidecommand
functions, as you should avoid redirecting here. (If you absolutely have to, you can return a{ redirect: location }
object and deal with it in the client.)Future work / open questions
Server caching
We want to provide some kind of caching mechanism down the line, which would give you the speed of prerendering data while also reacting to changes dynamically. If you're using Vercel, think of it as ISR on a function level.
We would love to hear your opinions on this matter and gather feedback around the other functions before committing to a solution.
Client caching
Right now a query is cached and deduplicated as long as there's one active subscription to it. Maybe you want to keep things around in memory a little longer, for example to make back/forward navigation instantaneous? We haven't explored this yet (but have some ideas) and would love to hear your use cases (or lack thereof) around this.
Prerendered data could be kept in memory as long as the page is open — since we know it’s unchanging, it never needs to be refetched. The downside is that the memory could then never be freed up. Perhaps this needs to be configurable.
Conversely, for queries that we know will become stale after a certain period of time, it would be useful if the query function could communicate to the client that it should be refetched after
n
seconds.Batching
We intend to add client-side batching (so that data from multiple queries is fetched in a single HTTP request) and server-side batching that solves the n + 1 problem, though this is not yet implemented.
Streaming
For real-time applications, we have a sketch of a primitive for streaming data from the server. We’d love to hear your use cases.
Beta Was this translation helpful? Give feedback.
All reactions