Generadores Asíncronos e Iteradores
Procesa grandes conjuntos de datos, APIs paginadas y datos de streaming con generadores asíncronos en Server Components y Route Handlers.
Receta
Referencia rápida para patrones de generadores asíncronos.
// Generador asíncronos básico
async function* fetchAllPages(baseUrl: string) {
let page = 1;
let hasMore = true;
while (hasMore) {
const res = await fetch(`${baseUrl}?page=${page}`);
const data = await res.json();
yield data.items;
hasMore = data.hasNextPage;
page++;
}
}
// Consumiendo con for-await-of
for await (const batch of fetchAllPages("/api/products")) {
processBatch(batch);
}Cuándo usarlo: Necesitas procesar respuestas de API paginadas, transmitir grandes conjuntos de datos sin cargar todo en memoria, o producir datos incrementalmente en un Route Handler.
Ejemplo Funcional
// lib/paginated-fetch.ts
interface PaginatedResponse<T> {
items: T[];
nextCursor: string | null;
}
async function* fetchAllItems<T>(
url: string,
options?: RequestInit
): AsyncGenerator<T[], void, unknown> {
let cursor: string | null = null;
do {
const fetchUrl = cursor ? `${url}?cursor=${cursor}` : url;
const res = await fetch(fetchUrl, options);
const data: PaginatedResponse<T> = await res.json();
yield data.items;
cursor = data.nextCursor;
} while (cursor !== null);
}
// app/admin/export/route.ts - Transmitir exportación CSV
import { NextResponse } from "next/server";
interface Order {
id: string;
customer: string;
total: number;
date: string;
}
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// Encabezado CSV
controller.enqueue(encoder.encode("id,customer,total,date\n"));
// Transmitir filas desde API paginada
for await (const batch of fetchAllItems<Order>(
"https://api.example.com/orders"
)) {
for (const order of batch) {
const row = `${order.id},${order.customer},${order.total},${order.date}\n`;
controller.enqueue(encoder.encode(row));
}
}
controller.close();
},
});
return new NextResponse(stream, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": 'attachment; filename="orders.csv"',
},
});
}Lo que esto demuestra:
- Generador asíncronos genérico para paginación basada en cursor
- Transmisión de exportación CSV a través de Route Handler
- Procesamiento de datos en lotes sin cargar todo en memoria
- Generador tipado con genéricos de TypeScript
Profundización
Cómo Funciona
- Una función generadora asíncronos usa la sintaxis
async function*yyieldpara producir valores yieldpausa el generador y devuelve un valor al consumidor- El consumidor usa
for await...ofpara iterar sobre valores cedidos - Cada iteración reanuda el generador hasta el siguiente
yieldoreturn - Eficiente en memoria: solo un lote está en memoria a la vez
- El cuerpo de la función generadora se ejecuta de forma perezosa (solo cuando se solicitan valores)
Parámetros y Valores de Retorno
| Concepto | Sintaxis | Descripción |
|---|---|---|
| Función generadora asíncronos | async function* name() | Define un generador que cede promesas |
yield | yield value | Pausa y produce un valor |
yield* | yield* otherGenerator() | Delega a otro generador |
for await...of | for await (const x of gen()) | Consume un iterable asíncronos |
return | return value | Termina el generador |
.next() | gen.next() | Avanza manualmente el generador |
.return() | gen.return() | Termina el generador temprano |
Variaciones
API paginada con offset:
async function* paginateWithOffset<T>(
fetcher: (offset: number, limit: number) => Promise<T[]>,
limit = 100
): AsyncGenerator<T[]> {
let offset = 0;
while (true) {
const batch = await fetcher(offset, limit);
if (batch.length === 0) break;
yield batch;
if (batch.length < limit) break; // Última página
offset += limit;
}
}
// Uso en un Server Component
export default async function AllProductsPage() {
const allProducts: Product[] = [];
for await (const batch of paginateWithOffset(
(offset, limit) =>
db.product.findMany({ skip: offset, take: limit }),
50
)) {
allProducts.push(...batch);
}
return <ProductGrid products={allProducts} />;
}Transmisión de Eventos Enviados por el Servidor (SSE):
// app/api/events/route.ts
export async function GET() {
const encoder = new TextEncoder();
async function* eventStream() {
let id = 0;
while (true) {
const data = await getLatestUpdate();
yield `id: ${id++}\ndata: ${JSON.stringify(data)}\n\n`;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
const stream = new ReadableStream({
async start(controller) {
for await (const event of eventStream()) {
controller.enqueue(encoder.encode(event));
}
},
});
return new NextResponse(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}Generador de transformación (patrón de canalización):
// Encadenar generadores para canalizaciones de transformación de datos
async function* map<T, U>(
source: AsyncIterable<T>,
transform: (item: T) => U | Promise<U>
): AsyncGenerator<U> {
for await (const item of source) {
yield await transform(item);
}
}
async function* filter<T>(
source: AsyncIterable<T>,
predicate: (item: T) => boolean | Promise<boolean>
): AsyncGenerator<T> {
for await (const item of source) {
if (await predicate(item)) yield item;
}
}
async function* take<T>(
source: AsyncIterable<T>,
count: number
): AsyncGenerator<T> {
let taken = 0;
for await (const item of source) {
yield item;
if (++taken >= count) break;
}
}
// Canalización: obtener todos los usuarios -> filtrar activos -> tomar los primeros 10 -> transformar
const pipeline = take(
map(
filter(
fetchAllItems<User>("https://api.example.com/users"),
(users) => users.filter((u) => u.active).length > 0
),
(users) => users.filter((u) => u.active)
),
10
);Recopilación de todos los resultados:
// Auxiliar para recopilar un iterable asíncronos en una matriz
async function collect<T>(iterable: AsyncIterable<T[]>): Promise<T[]> {
const results: T[] = [];
for await (const batch of iterable) {
results.push(...batch);
}
return results;
}
// Uso
const allOrders = await collect(
fetchAllItems<Order>("https://api.example.com/orders")
);Generador con límite de velocidad:
async function* rateLimited<T>(
source: AsyncIterable<T>,
delayMs: number
): AsyncGenerator<T> {
for await (const item of source) {
yield item;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
// Obtener páginas con retraso de 200 ms entre solicitudes
for await (const batch of rateLimited(fetchAllPages(url), 200)) {
processBatch(batch);
}Notas de TypeScript
// Tipificar generadores asíncronos
async function* counter(): AsyncGenerator<number, void, unknown> {
// AsyncGenerator<Yield, Return, Next>
// Yield = tipo de valores producidos por yield
// Return = tipo del valor de retorno
// Next = tipo de valores pasados a .next()
let i = 0;
while (true) {
yield i++;
}
}
// AsyncIterable es el tipo del lado del consumidor
async function processItems(source: AsyncIterable<string[]>) {
for await (const batch of source) {
console.log(batch.length);
}
}
// Tipo de obtención paginada genérica
type PaginatedFetcher<T> = (
cursor: string | null
) => Promise<{ items: T[]; nextCursor: string | null }>;Trampa
-
Los generadores son perezosos. Nada se ejecuta hasta que consumas el generador con
for await...ofo.next(). Si creas un generador pero nunca lo iteras, el cuerpo de la función nunca se ejecuta. Solución: Esta es una característica, no un error. Solo recuerda consumirlo. -
Manejo de errores en for-await-of. Si un yield lanza, el bucle termina y el error se propaga. Los elementos no consumidos se pierden. Solución: Envuelve el bucle en try/catch, o maneja los errores dentro del generador con try/catch alrededor de yield.
-
Fugas de memoria con generadores infinitos. Un generador que nunca regresa mantiene su closure vivo. Solución: Usa
breaken el consumidor o.return()en el generador para permitir la limpieza.for await...ofllama a.return()automáticamente enbreak. -
No se pueden usar generadores en Client Components. Los generadores se ejecutan en el servidor. Los Client Components necesitan los datos finales o un patrón de streaming (SSE, WebSocket). Solución: Usa generadores solo en Server Components, Route Handlers o Server Actions.
-
Sin contrapresión por defecto. El generador produce tan rápido como el consumidor solicita, pero si cedes a un stream, el stream podría almacenar en búfer. Solución: Agrega retrasos con
rateLimitedo usaReadableStreamcon contrapresión basada en extracción. -
Los generadores son de un solo uso. Una vez consumido, no puedes iterar un generador de nuevo. Llamar a la función generadora crea un nuevo iterador. Solución: Llama a la función generadora de nuevo para un iterador fresco.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Promise.all | Número fijo de obtenciones paralelas | Número desconocido de páginas |
| Array + bucle | Obtenciones secuenciales simples con conteo conocido | Conjuntos de datos grandes o sin límite |
ReadableStream | Respuestas HTTP de streaming (SSE, descargas de archivos) | Agregación de datos simple |
| Cursores de base de datos | Paginación directa de base de datos (Prisma, SQL) | Paginación de API externa |
| Web Streams API | Streaming compatible con navegador | Procesamiento solo en servidor |
Preguntas Frecuentes
¿Cuál es la diferencia entre una función asíncronos regular y un generador asíncronos?
- Una función asíncronos regular devuelve una única
Promiseque se resuelve en un valor - Un generador asíncronos usa
async function*yyieldpara producir múltiples valores en el tiempo - Los generadores son perezosos -- solo ejecutan código cuando el consumidor solicita el siguiente valor
¿Por qué los generadores son útiles para respuestas de API paginadas?
- Solo una página de datos está en memoria a la vez
- El generador se pausa entre páginas, manteniendo el uso de memoria constante
- El consumidor controla cuándo obtener la siguiente página a través de
for await...of
¿Puedes usar generadores asíncronos en Client Components?
- No. Los generadores se ejecutan solo en el servidor
- Los Client Components necesitan los datos finales o un patrón de streaming como SSE o WebSocket
- Usa generadores en Server Components, Route Handlers o Server Actions
¿Qué sucede si creas un generador pero nunca lo iteras?
- Nada se ejecuta. Los generadores son perezosos -- el cuerpo de la función nunca se ejecuta hasta que se consume
- Debes usar
for await...ofo llamar manualmente a.next()para iniciar la ejecución - Esta es una característica, no un error
¿Cómo tipificas una función generadora asíncronos en TypeScript?
async function* counter(): AsyncGenerator<number, void, unknown> {
// AsyncGenerator<Yield, Return, Next>
// Yield = tipo de valores producidos
// Return = tipo del valor de retorno final
// Next = tipo de valores pasados a .next()
let i = 0;
while (true) yield i++;
}¿Cómo manejas errores dentro de un bucle for await...of?
- Si un
yieldlanza, el bucle termina y el error se propaga - Los elementos no consumidos se pierden
- Envuelve el bucle en try/catch, o maneja los errores dentro del generador alrededor de cada
yield
¿Cuál es la diferencia entre yield y yield*?
yield valuepausa el generador y produce un valor únicoyield* otherGenerator()delega a otro generador, cediendo todos sus valoresyield*es útil para componer generadores
¿Cómo previene fugas de memoria con generadores infinitos?
- Usa
breaken el bucle consumidor o llama a.return()en el generador for await...ofllama automáticamente a.return()cuando hacesbreak- Sin limpieza, un generador infinito mantiene su closure vivo indefinidamente
¿Cómo transmite una exportación CSV usando un generador asíncronos en un Route Handler?
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode("id,name\n"));
for await (const batch of fetchAllItems<Item>(url)) {
for (const item of batch) {
controller.enqueue(encoder.encode(`${item.id},${item.name}\n`));
}
}
controller.close();
},
});
return new NextResponse(stream, {
headers: { "Content-Type": "text/csv" },
});
}¿Qué significa AsyncIterable vs AsyncGenerator en TypeScript?
AsyncIterable<T>es el tipo del lado del consumidor -- cualquier cosa que puedas usar confor await...ofAsyncGenerator<T>es el tipo del lado del productor devuelto porasync function*- Acepta
AsyncIterableen parámetros de función para flexibilidad
¿Cómo añades limitación de velocidad entre iteraciones del generador?
async function* rateLimited<T>(
source: AsyncIterable<T>,
delayMs: number
): AsyncGenerator<T> {
for await (const item of source) {
yield item;
await new Promise((r) => setTimeout(r, delayMs));
}
}¿Son los generadores de un solo uso o reutilizables?
- Los generadores son de un solo uso. Una vez consumido, no puedes iterarlos de nuevo
- Llamar a la función generadora de nuevo crea un iterador fresco
- Si necesitas iterar los mismos datos dos veces, llama a la función generadora dos veces
Relacionado
- Parallel Promises - Promise.all para obtenciones paralelas fijas
- Streaming - SSR basado en Suspense
- Server Actions - mutaciones y manejo de formularios
- SWR Pagination - desplazamiento infinito del lado del cliente