query
- La función
querypermite leer datos dinámicos del servidor - Para datos estáticos es recomendable usar la función
prerender - Las
queryno se pueden utilizar cuando toda la página está prerrenderizada (es decir cuandoexport const prerender = truese aplica a la página o a un layout principal), como cuando se usaadapter-static
Consulta
import { query } from "$app/server";
import * as db from "$lib/server/database";
export const getPosts = query(async () => {
const posts = await db. sql`
SELECT title, slug
FROM post
ORDER BY published_at
DESC
`;
return posts;
});
Manejo de resultados
- La consulta devuelta por
getPostsfunciona como unaPromiseque se resuelve enposts
<script lang="ts">
import { getPosts } from "./data.remote";
</script>
<h1>Recent posts</h1>
<ul>
{#each await getPosts() as { title, slug }}
<li><a href="/blog/{slug}">{title}</a></li>
{/each}
</ul>
- Hasta que la promesa se resuelva, y si se produce un error, se invocará el
<svelte:boundary>más cercano - Aunque se recomienda usar
await, como alternativa, la query también cuenta con las propiedadesloading,errorycurrent
<script>
import { getPosts } from "./data.remote";
const query = getPosts();
</script>
<h1>Recent posts</h1>
{#if query.error}
<p>oops!</p>
{:else if query.loading}
<p>loading...</p>
{:else}
<ul>
{#each query.current as { title, slug }}
<li><a href="/blog/{slug}">{title}</a></li>
{/each}
</ul>
{/if}
Argumentos de consulta
Las query functions pueden aceptar un argumento, como por ejemplo el slug de una publicación individual.
<script lang="ts">
import { getPost } from "../data.remote";
let { params } = $props();
const post = $derived(await getPost(params.slug));
</script>
<h1>{post.title}</h1>
<div>{@html post.content}</div>
Dado que getPost expone un endpoint HTTP, es importante validar este argumento para asegurarnos que sea del tipo correcto.
import * as v from "valibot";
import { error } from "@sveltejs/kit";
import { query } from "$app/server";
import * as db from "$lib/server/database";
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => {
const [ post ] = await db.sql`
SELECT *
FROM post
WHERE slug = ${slug}
`;
if (!post) error(404, "Not found");
return post;
});
Tanto el argumento como el valor de retorno se serializan con devalue, que mapea tipos como Data y Map (además de tipos personalizados definidos en el transport hook) además de JSON.
Para los argumentos de query y prerender (pero no para los valores de retorno), los objetos, mapas y conjuntos se ordenan de manera que instancias con los mismos miembros generen la misma clave de caché. Por ejemplo, getPosts({ limit: 10, offset: 10 }) y getPosts({ offset: 10, limit: 10 }) producirán la misma clave de caché. Si el orden es importante, se tendrá que usar un array.
Eliminación de duplicados (deduplicación)
Cuando se llama una función query, SvelteKit serializa el argumento con el que se la llama y lo utiliza como clave de caché.
- En el servidor, esto se utiliza para crear una caché con scope de solicitud, de modo que las múltiples invocaciones de la misma consulta solo se procesan una vez.
- En el cliente, SvelteKit hace algo similar: las multiples invocaciones idénticas de una consulta apuntan a la misma instancia.
Se puede usar un await sobre una query en cualquier contexto (componentes, handlers, funciones load universales , callbacks asíncronos) y SvelteKit hará la deduplicación con cualquier otro consumidor que esté usando esa misma query. Por ejemplo:
<script>
import { getData } from "data.remote.ts";
// - se espera (await) dentro del template del componente
// - llena la cache
const data = getData();
</script>
<p>{await data}</p>
// Esto se deduplica con el uso a nivel de componente de arriba
// Sin solicitud extra
<button onclick={async () => console.log(await getData())}>
Click
</button>
La caché se comparte mientras la query esté en uso activo; renderizada en un componente, en espera de respuesta (awaited) o referenciada de alguna otra forma. Una vez que nada o nadie la esté utilizando, el valor almacenado en caché se libera.
Actualizar queries
Cualquier consulta puede volver a obtenerse mediante su método refres(), que recupera el último valor del servidor.
<button onclick={() => getPosts().refres()}>
Check for new posts
</button>
Las queries se almacenan en caché mientras están en la página, lo que significa que getPosts() === getPosts(), esto implica que no se necesita una referencia como const posts = getPosts() para actualizar la query
query.batch
query.batch es una variante de query que resuelve el problema n+1: cuando se necesita una consulta individual por cada elemento de una lista (por ejemplo, el precio de cada producto en un carrito), normalmente se terminaría haciendo N llamadas separadas al servidor/base de datos.
Con query.batch, SvelteKit agrupa automáticamente todas las llamadas a esa query que ocurren dentro del mismo macrotask (por ejemplo, al renderizar una lista con {#each}) y las convierte en una sola petición al servidor.
Diferencia con query normal: en vez de recibir un argumento y devolver un resultado, el callback de query.batch:
- Recibe un array con todos los argumentos llamados durante ese lote.
- Debe devolver una función
(input, index) => output, que SvelteKit ejecuta una vez por cada llamada individual para repartir el resultado correspondiente.
import * as v from 'valibot';
import { query } from '$app/server';
import * as db from '$lib/server/database';
export const getProductInfo = query.batch(v.string(), async (skus) => {
// 1. Recibe un array con TODOS los argumentos del batch
// ej: ['SKU-001', 'SKU-002', 'SKU-003']
// 2. Hace UNA sola consulta con todos ellos
const products = await db.sql`
SELECT sku, price, stock, promo_id
FROM products
WHERE sku = ANY(${skus})
`;
// 3. Mapa para acceso rápido por ID
const lookup = new Map(products.map(p => [p.sku, p]));
// 4. Reparte el resultado a cada línea del carrito
return (sku) => lookup.get(sku) ?? { sku, price: null, stock: 0 };
});
v.string()valida que cada argumento sea unstring(elsku).- La consulta a la base de datos se ejecuta una sola vez, aunque
getProductInfo()se llame muchas veces con distintossku.
En el componente (ticket / carrito):
<script>
import { getProductInfo } from './products.remote';
let { cartItems } = $props();
</script>
<h2>Carrito</h2>
{#each cartItems as item}
{@const info = await getProductInfo(item.sku)}
<div class="line-item">
<span>{item.name}</span>
<span>Precio: {info.price}</span>
<span>Stock: {info.stock}</span>
</div>
{/each}
Aunque el {#each} llama a getProductInfo(item.sku) una vez por producto, SvelteKit detecta que todas esas llamadas ocurren en el mismo macrotask y las agrupa en una sola petición HTTP, resuelta en el servidor con una sola consulta (WHERE sku = ANY(...)).
Ejemplo: por qué importa en un POS: un ticket puede tener 5, 20 o más líneas renderizadas juntas. Sin query.batch, cada una dispararía su propia petición/consulta. Con query.batch, se reduce a una sola, lo cual importa para la latencia y la carga en la base de datos en un punto de venta.
Desde el componente, getProductInfo(sku) se usa igual que una query normal (con await) — es transparente. El batching ocurre por debajo, a nivel de red/servidor, sin cambiar cómo se consume en el template.
query.live
query.livesirve para acceder a datos en tiempo real desde el servidor.- Se comporta de forma similar a
query, pero el callback —normalmente una función generadora asíncrona— devuelve unAsyncIterable.
import { query } from "$app/server";
export const getTime = query.live( async function* () {
while (true) {
yield new Date();
await new Promise((f) => setTimeout(f, 1000));
}
});
- Durante el server-side rendering,
await getTime()devuelve el primer valor generado y luego cierra el iterador. Ese valor inicial se serializa y se reutiliza durante la hidratación. - En el cliente, la consulta permanece conectada mientras se esté usando activamente en un componente.
- Varias instancias comparten una misma conexión.
- Cuando ya no hay usos activos, el flujo se desconecta y se detiene la iteración del lado del servidor.
Las consultas en tiempo real exponen una propiedad connected y un método reconnect():
<script>
import { getTime } from "./time.remote.ts";
const time = getTime();
</script>
<p>{await time}</p>
<p>connected: {time.connected}</p>
<button onclick={() => time.reconnect()}>Reconnect</button>
- Si la conexión se cae,
connectedpasa a serfalse. - SvelteKit intentará reconectarse de forma pasiva con retroceso exponencial (backoff), y de forma activa si
navigator.onLinepasa defalseatrue. - A diferencia de las demás
query, las.liveno tienen métodorefresh(), ya que se actualizan automáticamente.
Si se necesita acceso directo e imperativo al stream subyacente de valores (en lugar de la propiedad reactiva current), las instancias de live query son iterables de forma asíncrona: se puede recorrer la instancia directamente con for await.
async function logTimes() {
for await (const value of getTime()) {
console.log(value);
if(someCondition) break;
}
}
Múltiples consumidores de la misma consulta en tiempo real —ya sea de forma reactiva (mediante await o current) o imperativa (mediante bucles for await)— comparten una única conexión subyacente. El primer valor que recibe un iterador for await es el valor más reciente disponible, si ya existe uno, reflejando el mismo comportamiento que esperar el recurso directamente. Las entregas posteriores se activan cada vez que llega un nuevo valor del servidor. Si los valores llegan más rápido de lo que el consumidor los procesa, solo se conserva el último valor pendiente: las transmisiones en tiempo real no son un registro de eventos.
En el servidor, for await también se une a una iteración compartida por solicitud (per-request) del generador subyacente, de modo que los consumidores concurrentes dentro de la misma solicitud no ejecutan varias veces el generador definido por el usuario.
Es fundamental no almacenar en caché las respuestas de live queries en un service worker, ya que la respuesta clonada seguirá transmitiéndose (streaming) mucho después de que la página se haya cerrado. Asegúrate de que tu lógica de caché excluya cualquier respuesta cuyo encabezado Cache-Control incluya no-store.