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.
Busca en todas las páginas de la documentación
Procesa grandes conjuntos de datos, APIs paginadas y datos de streaming con generadores asíncronos en Server Components y Route Handlers.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
async function* y yield para producir valoresyield pausa el generador y devuelve un valor al consumidorfor await...of para iterar sobre valores cedidosyield o return| 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 |
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);
}// 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 }>;Los generadores son perezosos. Nada se ejecuta hasta que consumas el generador con for await...of o .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 break en el consumidor o .return() en el generador para permitir la limpieza. for await...of llama a .return() automáticamente en break.
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 rateLimited o usa ReadableStream con 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.
| 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 |
Promise que se resuelve en un valorasync function* y yield para producir múltiples valores en el tiempofor await...offor await...of o llamar manualmente a .next() para iniciar la ejecuciónasync 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++;
}yield lanza, el bucle termina y el error se propagayieldyield value pausa el generador y produce un valor únicoyield* otherGenerator() delega a otro generador, cediendo todos sus valoresyield* es útil para componer generadoresbreak en el bucle consumidor o llama a .return() en el generadorfor await...of llama automáticamente a .return() cuando haces breakexport 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" },
});
}AsyncIterable<T> es el tipo del lado del consumidor -- cualquier cosa que puedas usar con for await...ofAsyncGenerator<T> es el tipo del lado del productor devuelto por async function*AsyncIterable en parámetros de función para flexibilidadasync 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));
}
}Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥