Skip to content

Commit f9b6237

Browse files
authored
docs: llm docs for await + remote functions (#1468)
1 parent 528841a commit f9b6237

File tree

2 files changed

+296
-0
lines changed

2 files changed

+296
-0
lines changed

apps/svelte.dev/src/routes/llms-small.txt/content-svelte.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,72 @@ let { a, b, ...others } = $props();
294294

295295
- Do **NOT** use this unless you are explicitly tasked to create a custom element using Svelte components
296296

297+
### Using await in Svelte
298+
299+
- **Where you can use await**
300+
301+
- **Top-level `<script>`**: `await` directly in component script.
302+
- **Inside `$derived(...)`**.
303+
- **Inside markup**: inline `await` expressions.
304+
305+
```svelte
306+
<script>
307+
import { getNumber, isEven, makeDouble } from './number';
308+
309+
let count = $state(0);
310+
let double = $derived(await makeDouble(double));
311+
</script>
312+
313+
<button onclick={() => count++}>increment<button>
314+
315+
<p>{await getNumber(count)} * 2 = {double}</p>
316+
{#if await isEven(id)}
317+
<p>even</p>
318+
{/if}
319+
```
320+
321+
- **Enable the feature**
322+
- Add `experimental.async: true` to `svelte.config.js`:
323+
324+
```js
325+
/// file: svelte.config.js
326+
export default {
327+
compilerOptions: {
328+
experimental: {
329+
async: true
330+
}
331+
}
332+
};
333+
```
334+
335+
- The flag is experimental in 5.36; it will be removed in Svelte 6.
336+
337+
- **Boundary requirement**
338+
- You can only use `await` inside a `<svelte:boundary>` that has a `pending` snippet:
339+
340+
```svelte
341+
<svelte:boundary>
342+
<MyApp />
343+
344+
{#snippet pending()}
345+
<p>loading...</p>
346+
{/snippet}
347+
</svelte:boundary>
348+
```
349+
350+
- Restriction will lift when async SSR is supported.
351+
352+
#### Behavior
353+
354+
- If an `await` depends on state, Svelte defers UI updates that read that state until the async work finishes.
355+
- Fast updates can overtake slow ones; results reflect the latest completed work.
356+
- **Script awaits are normal JS**: sequential unless you parallelize them yourself.
357+
- **$derived awaits**: first run sequentially, then update independently.
358+
- Expect an `await_waterfall` warning if you accidentally serialize independent work.
359+
- Boundary's `pending` snippet shows a loading placeholder while boundary first loads.
360+
- Use `$effect.pending()` to detect ongoing async work.
361+
- Errors from `await` bubble to the nearest `<svelte:boundary>` (acts as an error boundary).
362+
297363
### {#snippet ...}
298364

299365
- **Definition & Usage:**

apps/svelte.dev/src/routes/llms-small.txt/content-sveltekit.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,236 @@ Use it with a simple form:
441441
</form>
442442
```
443443

444+
## Remote functions (experimental)
445+
446+
- **What they are**: Type-safe server-only functions you call from the client. They always execute on the server, so they can access server-only modules (env, DB).
447+
- If you choose to use them you can replace load functions and form actions with them.
448+
- Works best in combination with asynchronous Svelte, i.e. using `await` expressions in `$derived` and template
449+
- **Opt-in**: Enable in `svelte.config.js`:
450+
451+
```js
452+
export default {
453+
kit: {
454+
experimental: {
455+
remoteFunctions: true
456+
}
457+
}
458+
};
459+
```
460+
461+
- **Where and how**:
462+
- Place `.remote.js`/`.remote.ts` files in `src/lib` or `src/routes`.
463+
- Export functions using one of: `query`, `form`, `command`, `prerender` from `$app/server`.
464+
- Client imports become fetch-wrappers to generated HTTP endpoints.
465+
- Arguments/returns are serialized with devalue (supports Date, Map, custom transport).
466+
467+
### query: read dynamic data
468+
469+
Define:
470+
471+
```js
472+
// src/routes/blog/data.remote.js
473+
import { query } from '$app/server';
474+
import * as db from '$lib/server/database';
475+
476+
export const getPosts = query(async () => {
477+
return db.posts();
478+
});
479+
```
480+
481+
Use in component (recommended with await):
482+
483+
```svelte
484+
<script>
485+
import { getPosts } from './data.remote';
486+
</script>
487+
488+
<ul>
489+
{#each await getPosts() as { title, slug }}
490+
<li><a href="/blog/{slug}">{title}</a></li>
491+
{/each}
492+
</ul>
493+
```
494+
495+
- **Args + validation**: Pass a Standard Schema (e.g. Valibot/Zod) as first param.
496+
497+
```js
498+
import * as v from 'valibot';
499+
export const getPost = query(v.string(), async (slug) => {
500+
/* ... */
501+
});
502+
```
503+
504+
- **Refresh/caching**: Calls are cached on page (`getPosts() === getPosts()`). Refresh via:
505+
506+
```svelte
507+
<button onclick={() => getPosts().refresh()}>Check for new posts</button>
508+
```
509+
510+
- Alternative props exist (`loading`, `error`, `current`) if you don’t use `await`.
511+
512+
### form: mutation via forms
513+
514+
Define:
515+
516+
```js
517+
import { form } from '$app/server';
518+
import * as db from '$lib/server/database';
519+
import * as auth from '$lib/server/auth';
520+
import { error, redirect } from '@sveltejs/kit';
521+
522+
export const createPost = form(async (data) => {
523+
const user = await auth.getUser();
524+
if (!user) error(401, 'Unauthorized');
525+
526+
const title = data.get('title');
527+
const content = data.get('content');
528+
db.insertPost(title, content);
529+
530+
redirect(303, `/blog/${title}`);
531+
});
532+
```
533+
534+
Use:
535+
536+
```svelte
537+
<script>
538+
import { createPost } from '../data.remote';
539+
</script>
540+
541+
<form {...createPost}>
542+
<input name="title" />
543+
<textarea name="content" />
544+
<button>Publish</button>
545+
</form>
546+
```
547+
548+
- **Progressive enhancement**: Works without JS via `method`/`action`; with JS it submits without full reload.
549+
- **Single-flight mutations**:
550+
- Server-driven: call refresh inside the handler:
551+
```js
552+
await getPosts().refresh();
553+
```
554+
- Client-driven: customize with `enhance` and `submit().updates(...)`:
555+
```svelte
556+
<form {...createPost.enhance(async ({ submit }) => {
557+
await submit().updates(getPosts());
558+
})}>
559+
```
560+
- Optimistic UI: use `withOverride`:
561+
```js
562+
await submit().updates(getPosts().withOverride((posts) => [newPost, ...posts]));
563+
```
564+
- **Returns**: Instead of redirect, return data; read at `createPost.result`.
565+
- **buttonProps**: For per-button `formaction`:
566+
567+
```svelte
568+
<button>login</button>
569+
<button {...register.buttonProps}>register</button>
570+
```
571+
572+
### command: programmatic writes
573+
574+
Define:
575+
576+
```js
577+
import { command, query } from '$app/server';
578+
import * as v from 'valibot';
579+
import * as db from '$lib/server/database';
580+
581+
export const getLikes = query(v.string(), async (id) => {
582+
return db.likes.get(id);
583+
});
584+
585+
export const addLike = command(v.string(), async (id) => {
586+
await db.likes.add(id);
587+
});
588+
```
589+
590+
Use:
591+
592+
```svelte
593+
<script>
594+
import { getLikes, addLike } from './likes.remote';
595+
let { item } = $props();
596+
</script>
597+
598+
<button onclick={() => addLike(item.id)}>add like</button>
599+
<p>likes: {await getLikes(item.id)}</p>
600+
```
601+
602+
- **Update queries**:
603+
- In the command: `getLikes(id).refresh()`
604+
- From client: `await addLike(item.id).updates(getLikes(item.id))`
605+
- Optimistic: `updates(getLikes(item.id).withOverride((n) => n + 1))`
606+
- **Note**: Cannot be called during render.
607+
608+
### prerender: build-time reads for static-ish data
609+
610+
Define:
611+
612+
```js
613+
import { prerender } from '$app/server';
614+
import * as db from '$lib/server/database';
615+
616+
export const getPosts = prerender(async () => {
617+
return db.sql`SELECT title, slug FROM post ORDER BY published_at DESC`;
618+
});
619+
```
620+
621+
- **Use anywhere** (including dynamic pages) to partially prerender data.
622+
- **Args + validation**: Same schema approach as `query`.
623+
- **Seed inputs**: Enumerate values for crawling during prerender:
624+
625+
```js
626+
export const getPost = prerender(
627+
v.string(),
628+
async (slug) => {
629+
/* ... */
630+
},
631+
{
632+
inputs: () => ['first-post', 'second-post']
633+
}
634+
);
635+
```
636+
637+
- **Dynamic**: By default excluded from server bundle; set `{ dynamic: true }` if you must call with non-prerendered args.
638+
- **Note**: If a page has `export const prerender = true` page option, you cannot use dynamic `query`s.
639+
640+
### Validation and security
641+
642+
- Use Standard Schema for `query`, `command`, `prerender` args to protect endpoints.
643+
- Failures return 400; customize with `handleValidationError`:
644+
645+
```ts
646+
// src/hooks.server.ts
647+
export function handleValidationError() {
648+
return { message: 'Nice try, hacker!' };
649+
}
650+
```
651+
652+
- `form` doesn’t take a schema (you validate `FormData` yourself).
653+
654+
### getRequestEvent inside remote functions
655+
656+
- Access the current `RequestEvent`:
657+
658+
```ts
659+
import { getRequestEvent, query } from '$app/server';
660+
661+
export const getProfile = query(async () => {
662+
const { cookies, locals } = getRequestEvent();
663+
// read cookies, reuse per-request work via locals, etc.
664+
});
665+
```
666+
667+
- Differences: no `params`/`route.id`, cannot set headers (except cookies, only in `form`/`command`), `url.pathname` is `/`.
668+
669+
### Redirects
670+
671+
- Allowed in `query`, `form`, `prerender` via `redirect(...)`.
672+
- Not allowed in `command` (return `{ redirect }` and handle on client if absolutely necessary).
673+
444674
## Page options
445675

446676
#### prerender

0 commit comments

Comments
 (0)