Remote Functions #13897
Replies: 156 comments 638 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.
-
A very basic setup with the sv cli tool including drizzle for database doesn't seem to build whenever a db call is used in the remote functions. Maybe I don't understand good enough, but I'd preferably use the dynamic over static
|
Beta Was this translation helpful? Give feedback.
-
Do I understand correctly that only queries have <script>
import { deleteItem }
let pending = $state(false)
let error = $state("")
async function onDeleteItem() {
pending = true
try {
await deleteItem()
} catch() {
error = err.message
} finally (err) {
pending = false
}
}
</script>
<button onclick={() => onDeleteItem()}>
{#if !pending}
Delete
{:else if}
Deleting <LoadingSpinner />
{/if}
</button>
{#if error}
<span> Error during deletion: {error}<span>
{/if}
Ideally I want something like this. Where <script>
import { deleteItem }
let deleteCmd = magicThingy(deleteItem)
</script>
<button onclick={() => deleteCmd.execute()}>
{#if !deleteCmd.pending}
Delete
{:else if}
Deleting <LoadingSpinner />
{/if}
</button>
{#if deleteCmd.error}
<span> Error during deletion: {error}<span>
{/if}
|
Beta Was this translation helpful? Give feedback.
-
Just created a new Svelte project with It works perfectly out-of-the-box, but when moving
Which is weird, not sure what's really happening here. Normally both form actions and remote functions run fully server-side, while it seems like Note that I use |
Beta Was this translation helpful? Give feedback.
-
I am struggling to understand how to maintain a high-performance app, something SvelteKit excels in, when queries depend on each other. I have an application that allows you to switch between workspaces, similar to Slack. The In the +page.server.ts approach, all of these queries happen in one request, and all of the data is sent to the client in one response for rendering (bonus, this also has pre-loading on link hover! Will remote functions ever get pre-loading?) Pre-loading and one HTTP request for data have the pages loading very quickly. With the Remote Functions, we have to first fetch the <script lang="ts">
import { getWorkspaceId, getWorkspaceData } from './data.remote';
let workspaceId = $derived(await getWorkspaceId());
// this has to wait for workspaceId first, delayed loading
let workspaceData = $derived(await(getWorkspaceData(workspaceId));
</script>
<!-- also has to wait for workspaceId -->
{await getWorkspaceData(workspaceId)} This feels like a step backwards in app performance. Is there a better way to do this? One thought was having a single remote function called something like getAllPageData() to get everything needed in a single HTTP request on first load, but then it feels like I might as well keep using page action loaders. This is a simple example, but you could have many different pieces of data that need to be fetched before other queries can run for page rendering such as: application settings, user info, user permissions, and more.....then the main page content can render. I feel like I am missing something obvious, and perhaps this is a documentation issue? |
Beta Was this translation helpful? Give feedback.
-
Is there a hook for RPC errors in the client? I need to catch any 401 error and log the user out. The |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
It would be really nice to have the ability to retrieve the Schema passed to the remote function. Validators are extremely useful in client-side code, and exporting them from a separate file rather than directly from where they are used (the remote file) is not very convenient. |
Beta Was this translation helpful? Give feedback.
-
For the cache feature. <script>
import { myQuery } from './my-query.remote'
const query = await myQuery(..., { policy: "cache-and-network" })
</script>
<p>{query.loading}</p>
<p>{query.error}</p>
<p>{query.data}</p> The main idea is that the data is coming from a client-side cache when available. |
Beta Was this translation helpful? Give feedback.
-
I really like the idea of form returning a form prop that is a svelte component. |
Beta Was this translation helpful? Give feedback.
-
I have a question about security authorization with remote functions. For the sake of this discussion, assume a route structure as follows:
The https://app.mysite.com/33efqef/home Load Functions and Form Actions Currently, we can use hook.server.ts to look at the import { Security } from '$lib/security';
export const handle: Handle = async ({ event, resolve }) => {
Security.checkUserAuth(event); // event inlcudes params.orgId slug
const response = await resolve(event);
return response;
}; I love this as it creates one centralized place to implement access controls. With this approach, we can continue to create many more pages and form actions without having to think about security, as it is built into every request with hook.server.ts. Remote Functions With remote functions, the hooks.server.ts function is run but it does not contain param/slug values, which make sense, but auth can no longer be done here. Now every single remote function will be responsible for implementing security: import { z } from 'zod';
import { query } from '$app/server';
const GetDataRequestSchema = z.object({
orgId: z.string(),
otherParams: z.string()
});
export const getData = query(GetDataRequestSchema, async (args) => {
Security.checkUserAuth(args.orgId);
// ... get the data
return data;
}); I'm not saying this is bad, but is there a better way? It is far too easy for a developer to forget to add this to every function, whereas with the hooks.server.ts approach, it "just worked" without additional cognitive load. If there is no better way to centralize auth, I'm thinking I will need to create a custom ESLint rule to ensure all remote functions have |
Beta Was this translation helpful? Give feedback.
-
Anyone else excited for the upcoming speed improvements? With async SSR, forking, and resources, I'd expect a small speedup for the initial load but a large speedup for page to page navigation (preloading & connecting directly to the database goes a long way). |
Beta Was this translation helpful? Give feedback.
-
What happens if I want to submit a remote form without refreshing any remote queries? Currently if I call A more concrete use-case is a chat app. I have a remote query that fetches the last N messages in a conversation, and a remote form that sends a message (and returns the whole message object). I'd like to just append this message to my local message state, not trigger a refresh of my Furthermore, is there any built-in way to change the local state of a remote query independently of any form submissions? Let's say I have a setup like this: <script lang="ts">
import { remoteGetChatMessages } from "$lib/remotes/chat.remote";
const messages = remoteGetChatMessages({ chatId: "XXX" });
</script>
{#if messages.loading}
<p>Loading...</p>
{:else if messages.error}
<p>Error: {messages.error.message}</p>
{:else}
<div>
{#each messages.current ?? [] as message}
<div>{message.content}</div>
{/each}
</div>
{/if} How am I meant to add new messages as they arrive from an e.g. WS/SSE stream? |
Beta Was this translation helpful? Give feedback.
-
Love this and super excited for it to stabilize!! Important question I can't help but have though – what does this mean for +page/+layout.ts functions, and when do you know when to use a remote function over a .ts file? Similar to the question that was raised when |
Beta Was this translation helpful? Give feedback.
-
I saw the PR about It appears to be trying to solve the n+1 problem in the client to reduce the number of HTTP requests to the server, and at the same time, on the server, reduce the n+1 problem by trying to reduce the number of database calls. It may not be possible to solve both of these at the same time, and it may require the discretion of the developer to determine implementation specifics.
In the PR docs there is the following example: <script>
import { getPost } from './batch.remote.js';
let { posts } = $props();
</script>
<h1>All my posts</h1>
<ul>
{#each posts as post}
<li>{await getPost(post.id).summary}</li>
{/each}
</ul> Combining these on the client side is great, one HTTP request. Combining these on the server and invoking the Remote Function once will be problematic. For the example above, you could easily split this into an array for the proper response structure: export const getPost = query.batch(v.string(), async (postIds: string[]) => {
const posts = await db.sql`
SELECT id, * FROM post
WHERE slug = ANY(${postIds})
`;
// Create a map for quick lookup
const postMap = new Map(posts.map(post => [post.id, post]));
// Return posts in the same order as postIds
return postIds.map(id => postMap.get(id));
}); Formatting the response data to make sure it aligns with the args array is doable in this simple example. This single method invocation approach breaks down quickly and does not work with more complex queries. Here is an example to show how this will not work for even marginally complex queries: Table Name: Account
.svelte file: {await getAccounts({country: us, minRev: 400000})
{await getAccounts({country: au, minRev: 12000000}) .remotes.ts: const accountFilterSchema = z.object({
country: z.string(),
minRev: z.number()
});
export const getAccounts = query.batch(accountFilterSchema, async (requests: z.infer<typeof accountFilterSchema>[]) => {
const posts = await db.sql`
SELECT * FROM account
WHERE (reqest1sql) OR (reqest2sql) OR (reqest3sql)
`;
// Uhh....this will return every row in the table, there is no way to know
// what rows returned in the query align to the requests.
}); The answer could be, just do a database query for each request in the array in the remote function....and exactly! This could be the default? For every request that is batched on the client, it should call the Remote Function once (in parallel) and automatically construct the response to the client in the correct order. The answer could also be: just use query...but then would I lose the client-side batching of combining calls into one http request? And I'm back to an app with a bunch of ajax spinners and SPA flashes as data loads on the client at different times? "But this will then cause the n+1 problem on the server" Yup, it will and there isn't a great way to solve this. With that said, Fwiw, batch on the client, but not on the server, and instead invoke functions one at a time with arguments in parallel is the way Salesforce built their Remote Actions and they have been used at massive scale for the last 13 years. Could also control batching client side if you don't want a load of a particular query slowed down server side and want it to execute on its own as fast as possible: {await getAccounts({country: us, minRev: 400000},{batch:false})
{await getAccounts({country: au, minRev: 12000000})
{await getAccounts({country: au, minRev: 0}) |
Beta Was this translation helpful? Give feedback.
-
for SEO purposes we need a way to tell sveltekit to wait for a single resource before flushing the stream or have a "async" mode which will enable that for all remote functions (disables streaming). even better we should have a way to set the mode to "stream" or "async" based on whether the user-agent is a bot, solid start allows this: // @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
import { isbot } from "isbot";
export default createHandler(
() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="en">{/* html... */}</html>
)}
/>
),
(e) => ({
mode: isbot(e.request.headers.get("user-agent")) ? "async" : "stream",
})
); |
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