FICHERO_VENTAS lleva declarado desde la lección anterior apuntando a datos/ventas.csv, un fichero que aún no existe. Ha llegado su momento, y con él el problema que ninguna de las herramientas vistas hasta ahora puede resolver.
El catálogo de Escena Viva ocupa dos kilobytes: readFile lo lee entero sin despeinarse. El histórico de ventas de una temporada es otra cosa. Tres salas, doscientas sesiones al año, miles de entradas por sesión, cada una con su código, su canal de compra y su marca de tiempo: cientos de megabytes. Y readFile no tiene forma de leer eso sin meterlo entero en memoria.
Los streams son la respuesta de Node a ese problema, y son mucho más que una optimización: son el modelo con el que Node piensa la entrada y la salida. Las peticiones y respuestas HTTP del Módulo 4 son streams; la compresión, la criptografía y las conexiones de red son streams. Y —esto va a sonarte— los streams son EventEmitter, así que todo lo de la lección 02-05 se aplica aquí directamente.
Contenido
- El problema: medir lo que cuesta
readFile - Qué es un stream y sus cuatro tipos
- Los streams son
EventEmitter - Modo fluido y modo pausado
createReadStream,createWriteStreamyhighWaterMark- Contrapresión: lo que devuelve
write() pipe()y su punto débil- El fichero
datos/ventas.csvde Escena Viva - Leer línea a línea con
readline for await...ofsobre un stream
- El problema: medir lo que cuesta
readFile
readFileNo hay que creerse esto de palabra. Vamos a medirlo.
// src/laboratorio/comparar-memoria.js
// Compara el consumo de memoria de readFile frente a un stream.
const fs = require('node:fs');
const fsPromesas = require('node:fs/promises');
const { FICHERO_VENTAS } = require('../config/rutas.js');
function memoriaMb() {
const { heapUsed, rss } = process.memoryUsage();
return { heapMb: +(heapUsed / 1048576).toFixed(1), rssMb: +(rss / 1048576).toFixed(1) };
}
async function conReadFile() {
const contenido = await fsPromesas.readFile(FICHERO_VENTAS, 'utf8');
return { modo: 'readFile', lineas: contenido.split('\n').length, ...memoriaMb() };
}
function conStream() {
return new Promise((resolver, rechazar) => {
let lineas = 0;
let resto = '';
const lector = fs.createReadStream(FICHERO_VENTAS, { encoding: 'utf8' });
lector.on('data', (trozo) => {
const partes = (resto + trozo).split('\n');
resto = partes.pop(); // el ultimo trozo puede estar cortado
lineas += partes.length;
});
lector.on('end', () => resolver({ modo: 'stream', lineas, ...memoriaMb() }));
lector.on('error', rechazar);
});
}Con un ventas.csv de 800 MB, la salida es demoledora:
┌─────────┬────────────┬──────────┬────────┬────────┐ │ (index) │ modo │ lineas │ heapMb │ rssMb │ ├─────────┼────────────┼──────────┼────────┼────────┤ │ 0 │ 'readFile' │ 9200001 │ 1621.4 │ 1712.8 │ │ 1 │ 'stream' │ 9200001 │ 6.2 │ 58.3 │ └─────────┴────────────┴──────────┴────────┴────────┘
Y si el fichero fuera algo mayor, readFile ni siquiera llegaría a imprimir: fallaría con ERR_STRING_TOO_LONG —una cadena de V8 tiene un límite de unos 512 MB— o con JavaScript heap out of memory, que mata el proceso entero. La diferencia es de dos órdenes de magnitud, y la explicación es simple: readFile necesita tener el fichero completo en memoria a la vez, mientras que el stream lo procesa en trozos de 64 KB y descarta cada uno en cuanto lo ha usado. El consumo del stream no depende del tamaño del fichero: esa es la propiedad clave, memoria constante.
- Qué es un stream y sus cuatro tipos
Un stream es una secuencia de datos que se procesa por partes, a medida que están disponibles, en lugar de esperar a tenerlos todos. La analogía del agua es buena: readFile es llenar la bañera entera antes de tocar el agua; un stream es abrir el grifo y trabajar con lo que va saliendo.
| Tipo | Qué hace | Ejemplos en Node |
|---|---|---|
| Readable | Produce datos que tú consumes | fs.createReadStream, process.stdin, el cuerpo de una petición HTTP |
| Writable | Consume datos que tú produces | fs.createWriteStream, process.stdout, la respuesta HTTP |
| Duplex | Lectura y escritura independientes | Un socket TCP (net.Socket) |
| Transform | Duplex donde la salida es función de la entrada | zlib.createGzip, cifrado, tu propio parser de CSV |
Los Transform son el tema de la lección siguiente. Aquí nos concentramos en los dos primeros, que son la base de todo lo demás.
- Los streams son
EventEmitter
EventEmitterEsto no es una analogía: Readable y Writable heredan de EventEmitter. on, once, emit, off y todo lo de la lección 02-05 funciona tal cual, incluida la regla de oro que ahora cobra su verdadero sentido.
| Evento | Stream | Cuándo se emite |
|---|---|---|
data |
Readable | Hay un trozo disponible. Suscribirse activa el modo fluido |
end |
Readable | No quedan más datos que leer |
error |
Ambos | Fallo en la operación. Si no lo escuchas, tumba el proceso |
close |
Ambos | El descriptor subyacente se ha cerrado y liberado |
finish |
Writable | Se llamó a end() y todo lo pendiente se ha volcado |
drain |
Writable | El búfer interno se ha vaciado: puedes volver a escribir |
Dos advertencias que valen por media lección. La primera: end y finish no son lo mismo. end es del lado que lee ("se acabó la entrada") y finish es del lado que escribe ("he terminado de volcar"); confundirlos lleva a cerrar un fichero antes de que sus datos estén en disco. La segunda: el evento error de un stream sigue la regla del EventEmitter que ya conoces —un error sin oyentes lanza una excepción no capturada y mata el proceso—. Un stream sin on('error') es una caída de producción esperando a ocurrir, y el caso más habitual no es un disco roto: es un ENOENT porque el fichero no estaba.
- Modo fluido y modo pausado
Un Readable tiene dos formas de entregar datos, y esta dualidad es la fuente de la mitad de los problemas de quien empieza con streams.
| Modo fluido (flowing) | Modo pausado (paused) | |
|---|---|---|
| Cómo se activa | on('data'), pipe() o resume() |
Es el estado inicial; se vuelve con pause() |
| Quién marca el ritmo | El stream: empuja los datos | Tú: los pides con read() |
| Cómo se consume | Callback de data |
on('readable') + read() |
| Riesgo | Si tardas en procesar, los datos siguen llegando | Olvidar llamar a read() y quedarte parado |
// Modo fluido: el stream empuja.
lector.on('data', (trozo) => procesar(trozo));
// Modo pausado: tu tiras.
lector.on('readable', () => {
let trozo;
while ((trozo = lector.read()) !== null) procesar(trozo);
});No los mezcles. Suscribirse a data y llamar a read() en el mismo stream produce comportamientos difíciles de razonar: trozos que se pierden, end que no llega, orden alterado. Elige un modo por stream y mantenlo —aunque en la práctica la recomendación es no usar ninguno directamente: pipe, pipeline o for await...of resuelven el problema mejor—. Y un tercer detalle del modo fluido: si te suscribes a data después de un await, puedes perder trozos, porque el stream empezó a fluir antes de que llegara tu oyente. Suscribe siempre de forma síncrona, justo después de crear el stream.
createReadStream, createWriteStream y highWaterMark
createReadStream, createWriteStream y highWaterMarkconst fs = require('node:fs');
const lector = fs.createReadStream(FICHERO_VENTAS, {
encoding: 'utf8', // sin esto, los trozos son Buffer
highWaterMark: 64 * 1024 // tamano del bufer interno: 64 KB (por defecto)
});
const escritor = fs.createWriteStream(rutaSalida, { flags: 'a' }); // 'a': anadirEl highWaterMark es el tamaño del búfer interno: cuántos bytes acumula el stream antes de considerar que ya tiene bastante. Bajarlo (16 KB) gasta menos memoria por stream a cambio de más llamadas al sistema y más eventos; subirlo (1 MB) hace lo contrario. Los 64 KB por defecto son razonables casi siempre: bajarlo tiene sentido cuando manejas miles de streams simultáneos —un servidor con muchas conexiones— y subirlo cuando procesas pocos ficheros muy grandes. En modo objeto (que verás en la lección siguiente) el highWaterMark cuenta objetos, no bytes, y vale 16 por defecto.
Una advertencia sobre encoding: si no lo indicas, los trozos llegan como Buffer; y si lo indicas como 'utf8', el stream se encarga de que ningún carácter multibyte quede partido entre dos trozos —un problema real que veremos a fondo en la lección 03-06 con StringDecoder—.
- Contrapresión: lo que devuelve
write()
write()Aquí está el concepto que separa a quien usa streams de quien los entiende. Imagina que lees de un SSD rápido y escribes en un disco lento o en una conexión de red: los datos entran a 500 MB/s y salen a 50 MB/s. ¿Dónde va la diferencia? Al búfer interno del stream de escritura, que crece sin parar hasta agotar la memoria. Es decir: has cambiado un readFile que consumía 800 MB por un stream que consume... 800 MB.
La contrapresión (backpressure) es el mecanismo que impide eso, y se apoya en un detalle que casi todo el mundo ignora:
const sePuedeSeguir = escritor.write(trozo);
// true -> el bufer tiene sitio, sigue escribiendo
// false -> el bufer esta LLENO. Deja de escribir y espera al evento 'drain'write() devuelve un booleano. Y ese booleano no es informativo: es una orden.
// MAL: ignorar el valor de retorno.
lector.on('data', (trozo) => {
escritor.write(trozo); // devuelve false y a nadie le importa
});Ese código funciona con ficheros pequeños y revienta con los grandes: el búfer del escritor crece indefinidamente porque nadie deja de alimentarlo. Es el fallo más común al escribir streams a mano, y el más difícil de diagnosticar, porque en desarrollo —fichero pequeño, disco rápido— no se manifiesta nunca.
// BIEN: respetar la contrapresion.
lector.on('data', (trozo) => {
if (!escritor.write(trozo)) {
lector.pause(); // deja de leer
escritor.once('drain', () => lector.resume()); // reanuda al vaciarse
}
});
lector.on('end', () => escritor.end());El ciclo completo es: data → write() devuelve false → pause() → el búfer se vacía y el escritor emite drain → resume() → vuelta a data. El resultado es que el consumidor lento marca el ritmo del productor rápido, y la memoria se mantiene acotada por el highWaterMark. Esta idea no es exclusiva de los ficheros: es la misma que gobierna TCP y la que hará que un cliente lento no tumbe tu servidor HTTP en el Módulo 4.
pipe() y su punto débil
pipe() y su punto débilEscribir a mano el baile de pause/resume/drain en cada tubería sería insoportable. pipe() lo hace por ti:
Esa línea equivale a todo el bloque anterior: conecta ambos streams, gestiona la contrapresión y llama a escritor.end() cuando el lector termina. Se pueden encadenar —a.pipe(b).pipe(c)— porque pipe devuelve el stream de destino.
Pero pipe tiene un defecto grave y poco conocido: no propaga errores ni limpia lo que deja atrás. Si lector falla —un ENOENT, un disco con errores—, el error no llega a escritor, que queda abierto con su descriptor sin liberar; y si nadie escucha error en lector, el proceso muere.
Con una tubería de tres o cuatro etapas el problema se multiplica: hay que suscribir on('error') en cada stream y cerrar a mano los que queden colgando. Es tedioso, es fácil olvidarse de uno, y ese olvido se paga en descriptores filtrados hasta el EMFILE.
Por eso la recomendación moderna es rotunda: usa pipe() para entender el concepto y stream.pipeline para trabajar. pipeline propaga errores, destruye todos los streams de la cadena cuando algo falla y tiene versión con promesas. Es el tema central de la lección siguiente.
- El fichero
datos/ventas.csv de Escena Viva
datos/ventas.csv de Escena VivaNecesitamos datos con los que trabajar. Este es el formato del histórico de ventas:
codigoEntrada,sesionId,fechaVenta,precioCentimos,canal EV-2026-000001,ses-001-1,2026-06-14T10:22:41,2500,web EV-2026-000002,ses-001-1,2026-06-14T10:23:07,2500,web EV-2026-000003,ses-002-1,2026-06-14T11:02:55,1800,taquilla EV-2026-000004,ses-003-2,2026-06-15T09:41:12,4200,app
Las columnas siguen las convenciones del proyecto: codigoEntrada con el formato EV-<año>-<6 dígitos>, sesionId con la forma ses-NNN-M, fechaVenta como cadena ISO sin zona, precioCentimos entero y canal con uno de tres valores (web, taquilla, app). Generamos un fichero coherente con la semilla: exactamente 1811 ventas, repartidas según las vendidas de cada sesión.
// src/laboratorio/generar-ventas.js
// Genera datos/ventas.csv coherente con datos/eventos.json.
const fs = require('node:fs');
const { obtenerCatalogo } = require('../catalogo-datos.js');
const { FICHERO_VENTAS } = require('../config/rutas.js');
const CANALES = ['web', 'taquilla', 'app'];
async function principal() {
const eventos = await obtenerCatalogo();
const escritor = fs.createWriteStream(FICHERO_VENTAS, { encoding: 'utf8' });
escritor.write('codigoEntrada,sesionId,fechaVenta,precioCentimos,canal\n');
let numero = 0;
for (const evento of eventos) {
for (const sesion of evento.sesiones) {
for (let i = 0; i < sesion.vendidas; i += 1) {
numero += 1;
const codigo = `EV-2026-${String(numero).padStart(6, '0')}`;
const canal = CANALES[numero % CANALES.length];
// Ventas repartidas por minutos desde la apertura de taquilla.
const fecha = new Date(2026, 5, 14, 10, numero % 600).toISOString().slice(0, 19);
// La contrapresion se ignora aqui a proposito: son 1811 lineas.
escritor.write(`${codigo},${sesion.id},${fecha},${sesion.precioCentimos},${canal}\n`);
}
}
}
escritor.end();
// 'finish' llega cuando TODO se ha volcado al disco, no al acabar end().
escritor.on('finish', () => console.error(`[ventas] ${numero} ventas escritas`));
}
if (require.main === module) principal();node src/laboratorio/generar-ventas.js
# [ventas] 1811 ventas escritas
wc -l datos/ventas.csv
# 1812 datos/ventas.csv (1811 ventas + cabecera)El comentario sobre la contrapresión es deliberado: con 1811 líneas no hay problema, pero el código correcto para volúmenes reales tendría que respetar el retorno de write() o, mejor, construirse con pipeline y un generador, como haremos en la lección siguiente.
- Leer línea a línea con
readline
readlineUn CSV se procesa por líneas, pero los trozos de un stream no coinciden con las líneas: un trozo de 64 KB corta por la mitad la línea que le toque. En el laboratorio del apartado 1 lo resolvimos a mano con una variable resto; el módulo node:readline lo hace bien y sin código propio.
// src/informes/ventas-por-sesion.js
// Recorre datos/ventas.csv linea a linea y acumula el total por sesion.
const fs = require('node:fs');
const readline = require('node:readline');
const { FICHERO_VENTAS } = require('../config/rutas.js');
async function totalizarPorSesion(ruta = FICHERO_VENTAS) {
const lector = readline.createInterface({
input: fs.createReadStream(ruta, { encoding: 'utf8' }),
crlfDelay: Infinity // trata \r\n como un solo salto (ficheros de Windows)
});
const porSesion = new Map();
let numeroLinea = 0;
for await (const linea of lector) {
numeroLinea += 1;
if (numeroLinea === 1 || linea.trim() === '') continue; // cabecera y blancos
const [, sesionId, , precioCentimos, canal] = linea.split(',');
if (!sesionId || Number.isNaN(Number(precioCentimos))) {
// Diagnostico por stderr: una linea corrupta no aborta el informe.
console.error(`[ventas] linea ${numeroLinea} ignorada: ${linea}`);
continue;
}
const acumulado = porSesion.get(sesionId) ??
{ sesionId, entradas: 0, recaudacionCentimos: 0, canales: new Set() };
acumulado.entradas += 1;
acumulado.recaudacionCentimos += Number(precioCentimos);
acumulado.canales.add(canal);
porSesion.set(sesionId, acumulado);
}
return [...porSesion.values()]
.map((a) => ({ ...a, canales: [...a.canales].join('/') }))
.sort((a, b) => a.sesionId.localeCompare(b.sesionId));
}
module.exports = { totalizarPorSesion };node -e "require('./src/informes/ventas-por-sesion.js').totalizarPorSesion().then(console.table)"
# ses-001-1 180 entradas 450000 centimos web/taquilla/app
# ses-001-2 96 entradas 211200 centimos web/taquilla/app
# ses-002-1 118 entradas 212400 centimos web/taquilla/app
# ...Tres decisiones dignas de mención. crlfDelay: Infinity evita que un fichero generado en Windows produzca líneas terminadas en \r invisible que estropea el último campo. Una línea corrupta se registra y se salta, no aborta el informe: en un histórico de millones de registros siempre hay basura, y un informe que muere en la línea 4.000.000 no sirve de nada. Y el consumo de memoria es constante: porSesion guarda una entrada por sesión (siete), no por venta.
for await...of sobre un stream
for await...of sobre un streamEse for await (const linea of lector) merece explicación, porque es la forma más legible de consumir un stream y ya la conoces de la lección 02-05, cuando viste events.on().
Todo Readable es un iterable asíncrono. Se puede recorrer directamente:
// Consumo con for await: sin callbacks, sin pause/resume manual.
const lector = fs.createReadStream(FICHERO_VENTAS, { encoding: 'utf8' });
let bytes = 0;
for await (const trozo of lector) {
bytes += trozo.length;
}
console.log(`${bytes} caracteres leidos`);Sus ventajas sobre on('data') son concretas: la contrapresión es automática (el bucle no pide el siguiente trozo hasta terminar el cuerpo) en lugar de manual; los errores se capturan con un try/catch normal alrededor del bucle en lugar de con un on('error') aparte; se puede usar await dentro; y un break destruye el stream solo, sin destroy() explícito.
Ese await dentro del bucle es la diferencia decisiva. Si por cada línea del CSV hay que consultar una base de datos, con on('data') las consultas se lanzarían todas a la vez y hundirían el servidor; con for await, la lectura se detiene sola hasta que la consulta termina, así que la contrapresión sale gratis. El precio es rendimiento bruto: for await es algo más lento porque cada iteración crea una promesa. Para un proceso por lotes es irrelevante; en una ruta crítica con mucho tráfico, mídelo.
Errores Comunes y Consejos
- No suscribir
on('error'). Unerrorsin oyentes en unEventEmittermata el proceso, y un stream es unEventEmitter. La solución definitiva espipeline. - Ignorar el retorno de
write(). Funciona en desarrollo y agota la memoria en producción. Si escribes a mano, respeta eldrain. - Confundir
endconfinish.endes el lector agotado;finishes el escritor vaciado. Cerrar por el evento equivocado corta datos. - Suponer que un trozo es una línea. Nunca lo es. Usa
readlineo unTransformque trocee. - Mezclar modo fluido y pausado, o suscribirse a
datatras unawait. En el primer caso el comportamiento es errático; en el segundo pierdes trozos. - Acumular todos los trozos en un array para juntarlos al final. Eso es
readFilecon pasos extra: has perdido la única ventaja del stream. - Consejo: si el fichero cabe holgadamente en memoria y solo lo lees una vez,
readFilees más simple y más rápido; los streams son para lo grande, lo infinito o lo que llega poco a poco. Y cuando dudes de si hay contrapresión, imprimeescritor.writableLengthde vez en cuando: si crece sin parar, la estás ignorando.
Ejercicios
Ejercicio 1: informe de ventas por canal y por hora
Amplía src/informes/ventas-por-sesion.js con una función resumirPorCanal() que recorra datos/ventas.csv una sola vez y devuelva, por canal, el número de entradas, la recaudación total y la hora punta (la hora del día con más ventas). Debe usar readline, mantener memoria constante y validar que la suma de entradas de todos los canales es 1811.
Ejercicio 2: copia con contrapresión manual
Escribe src/laboratorio/copiar-con-contrapresion.js que copie un fichero grande sin usar pipe, respetando el drain, e imprima cada segundo: megabytes copiados, writableLength del escritor y número de pausas por contrapresión. Ejecuta después una versión que ignore el retorno de write() y compara el consumo de memoria con process.memoryUsage().rss.
Ejercicio 3: filtrar ventas de una sala
Escribe src/laboratorio/filtrar-ventas.js que lea datos/ventas.csv, se quede solo con las ventas cuyas sesiones pertenezcan a una sala dada (--sala="Sala Boveda", cruzando con el catálogo) y escriba el resultado en datos/ventas-<sala>.csv conservando la cabecera. Usa readline para leer y un createWriteStream para escribir, respetando la contrapresión.
Soluciones
Solución 1. La clave es acumular en dos niveles durante el mismo recorrido:
for await (const linea of lector) {
// ...saltar cabecera y lineas vacias como antes...
const [, , fechaVenta, precioCentimos, canal] = linea.split(',');
const acumulado = porCanal.get(canal) ??
{ canal, entradas: 0, recaudacionCentimos: 0, porHora: new Array(24).fill(0) };
acumulado.entradas += 1;
acumulado.recaudacionCentimos += Number(precioCentimos);
// 'AAAA-MM-DDTHH:mm:ss' -> la hora esta en las posiciones 11 y 12.
acumulado.porHora[Number(fechaVenta.slice(11, 13))] += 1;
porCanal.set(canal, acumulado);
}
return [...porCanal.values()].map(({ porHora, ...resto }) => ({
...resto,
horaPunta: porHora.indexOf(Math.max(...porHora))
}));El array de 24 posiciones por canal es el truco que mantiene la memoria constante: no se guardan las ventas, solo el contador de cada hora. Un Map de fechas completas crecería con el fichero.
Solución 2. El núcleo de la copia manual:
lector.on('data', (trozo) => {
copiados += trozo.length;
if (!escritor.write(trozo)) {
pausas += 1;
lector.pause();
escritor.once('drain', () => lector.resume());
}
});
lector.on('end', () => escritor.end());
lector.on('error', (e) => { console.error(e); escritor.destroy(); });
escritor.on('error', (e) => { console.error(e); lector.destroy(); });Con la contrapresión respetada, writableLength oscila alrededor del highWaterMark y el rss se mantiene plano. Sin ella, writableLength crece sin límite y el rss sube hasta acercarse al tamaño del fichero. Fíjate también en los dos on('error') cruzados que hacen falta para no filtrar descriptores: ese trabajo manual es exactamente lo que pipeline automatiza.
Solución 3. Lo importante es cruzar el catálogo antes de empezar a leer el CSV, para no hacerlo por línea:
const eventos = await obtenerCatalogo();
const sesionesDeLaSala = new Set(
eventos.filter((e) => e.sala === sala).flatMap((e) => e.sesiones.map((s) => s.id))
);
const escritor = fs.createWriteStream(rutaSalida, { encoding: 'utf8' });
let primera = true;
for await (const linea of lector) {
if (primera) { escritor.write(`${linea}\n`); primera = false; continue; }
if (!sesionesDeLaSala.has(linea.split(',')[1])) continue;
// for await ya aplica contrapresion a la LECTURA; aqui la aplicamos
// a la ESCRITURA esperando el drain cuando el bufer se llena.
if (!escritor.write(`${linea}\n`)) {
await new Promise((resolver) => escritor.once('drain', resolver));
}
}
escritor.end();Ese await new Promise(... 'drain' ...) es la traducción de la contrapresión al mundo de async/await, y es un patrón que merece la pena memorizar: dentro de un for await, esperar el drain detiene también la lectura, porque el bucle no pide el siguiente elemento hasta que termina el cuerpo. Un Set con los ids de sesión evita recorrer el catálogo 1811 veces.
Conclusión
Ya sabes por qué existen los streams y qué problema resuelven exactamente: has medido la diferencia entre 1621 MB de readFile y 6 MB de un stream sobre el mismo fichero, y entiendes que el consumo de un stream no depende del tamaño de la entrada. Conoces los cuatro tipos —Readable, Writable, Duplex y Transform— y el hecho fundamental de que son EventEmitter, con data, end, error, close, finish y drain, sin confundir end con finish y sabiendo que un error sin oyentes tumba el proceso.
Distingues el modo fluido del pausado y por qué no deben mezclarse; sabes qué es el highWaterMark y qué se gana y se pierde al moverlo. Y, sobre todo, entiendes la contrapresión: que write() devuelve un booleano que es una orden, que ignorarlo convierte tu stream en un readFile disfrazado, y que el ciclo pause → drain → resume es lo que hace que el consumidor lento marque el ritmo. Sabes que pipe() automatiza ese baile pero no propaga errores ni limpia descriptores, y por eso no es la herramienta final. Escena Viva, mientras tanto, tiene ya su datos/ventas.csv con las 1811 ventas de la semilla y un src/informes/ventas-por-sesion.js que lo recorre línea a línea con readline, acumulando entradas, recaudación y canales por sesión con memoria constante. Y has visto la forma más legible de consumir cualquier stream: for await...of, que trae la contrapresión de regalo y permite await dentro del bucle.
En Streams de Transformación y pipeline cerramos el círculo. Aprenderás a escribir tus propios streams con Transform (_transform y _flush), a montar tuberías de proceso completas —CSV crudo a objetos, filtrado por sala, agregación por sesión, escritura del informe—, y a sustituir pipe() por stream.pipeline, que propaga errores, destruye toda la cadena cuando algo falla y tiene versión con promesas. Verás además Readable.from(), los generadores asíncronos como transformaciones, la compresión con zlib intercalada en la tubería y la cancelación con AbortSignal.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
