FITXER_VENDES fa des de la lliçó anterior que està declarat apuntant a dades/vendes.csv, un fitxer que encara no existeix. Ha arribat el seu moment, i amb ell el problema que cap de les eines vistes fins ara no pot resoldre.
El catàleg d'Escena Viva ocupa dos kilobytes: readFile el llegeix sencer sense despentinar-se. L'històric de vendes d'una temporada és una altra cosa. Tres sales, dues-centes sessions l'any, milers d'entrades per sessió, cadascuna amb el seu codi, el seu canal de compra i la seva marca de temps: centenars de megabytes. I readFile no té manera de llegir això sense ficar-ho sencer a la memòria.
Els streams són la resposta de Node a aquest problema, i són molt més que una optimització: són el model amb què Node pensa l'entrada i la sortida. Les peticions i respostes HTTP del Mòdul 4 són streams; la compressió, la criptografia i les connexions de xarxa són streams. I —això et sonarà— els streams són EventEmitter, així que tot el de la lliçó 02-05 s'aplica aquí directament.
Contingut
- El problema: mesurar el que costa
readFile - Què és un stream i els seus quatre tipus
- Els streams són
EventEmitter - Mode fluid i mode pausat
createReadStream,createWriteStreamihighWaterMark- Contrapressió: el que torna
write() pipe()i el seu punt feble- El fitxer
dades/vendes.csvd'Escena Viva - Llegir línia a línia amb
readline for await...ofsobre un stream
- El problema: mesurar el que costa
readFile
readFileNo cal creure's això de paraula. Anem a mesurar-ho.
// src/laboratori/comparar-memoria.js
// Compara el consum de memoria de readFile davant d'un stream.
const fs = require('node:fs');
const fsPromeses = require('node:fs/promises');
const { FITXER_VENDES } = require('../config/rutes.js');
function memoriaMb() {
const { heapUsed, rss } = process.memoryUsage();
return { heapMb: +(heapUsed / 1048576).toFixed(1), rssMb: +(rss / 1048576).toFixed(1) };
}
async function ambReadFile() {
const contingut = await fsPromeses.readFile(FITXER_VENDES, 'utf8');
return { mode: 'readFile', linies: contingut.split('\n').length, ...memoriaMb() };
}
function ambStream() {
return new Promise((resoldre, rebutjar) => {
let linies = 0;
let resta = '';
const lector = fs.createReadStream(FITXER_VENDES, { encoding: 'utf8' });
lector.on('data', (tros) => {
const parts = (resta + tros).split('\n');
resta = parts.pop(); // l'ultim tros pot estar tallat
linies += parts.length;
});
lector.on('end', () => resoldre({ mode: 'stream', linies, ...memoriaMb() }));
lector.on('error', rebutjar);
});
}Amb un vendes.csv de 800 MB, la sortida és demolidora:
┌─────────┬────────────┬──────────┬────────┬────────┐ │ (index) │ mode │ linies │ heapMb │ rssMb │ ├─────────┼────────────┼──────────┼────────┼────────┤ │ 0 │ 'readFile' │ 9200001 │ 1621.4 │ 1712.8 │ │ 1 │ 'stream' │ 9200001 │ 6.2 │ 58.3 │ └─────────┴────────────┴──────────┴────────┴────────┘
I si el fitxer fos una mica més gran, readFile ni tan sols arribaria a imprimir: fallaria amb ERR_STRING_TOO_LONG —una cadena de V8 té un límit d'uns 512 MB— o amb JavaScript heap out of memory, que mata el procés sencer. La diferència és de dos ordres de magnitud, i l'explicació és simple: readFile necessita tenir el fitxer complet a la memòria alhora, mentre que l'stream el processa en trossos de 64 KB i en descarta cadascun tan bon punt l'ha fet servir. El consum de l'stream no depèn de la mida del fitxer: aquesta és la propietat clau, memòria constant.
- Què és un stream i els seus quatre tipus
Un stream és una seqüència de dades que es processa per parts, a mesura que estan disponibles, en comptes d'esperar a tenir-les totes. L'analogia de l'aigua és bona: readFile és omplir la banyera sencera abans de tocar l'aigua; un stream és obrir l'aixeta i treballar amb el que va sortint.
| Tipus | Què fa | Exemples a Node |
|---|---|---|
| Readable | Produeix dades que tu consumeixes | fs.createReadStream, process.stdin, el cos d'una petició HTTP |
| Writable | Consumeix dades que tu produeixes | fs.createWriteStream, process.stdout, la resposta HTTP |
| Duplex | Lectura i escriptura independents | Un sòcol TCP (net.Socket) |
| Transform | Duplex on la sortida és funció de l'entrada | zlib.createGzip, xifratge, el teu propi analitzador de CSV |
Els Transform són el tema de la lliçó següent. Aquí ens concentrem en els dos primers, que són la base de tota la resta.
- Els streams són
EventEmitter
EventEmitterAixò no és cap analogia: Readable i Writable hereten d'EventEmitter. on, once, emit, off i tot el de la lliçó 02-05 funciona tal qual, inclosa la regla d'or que ara pren el seu veritable sentit.
| Esdeveniment | Stream | Quan s'emet |
|---|---|---|
data |
Readable | Hi ha un tros disponible. Subscriure-s'hi activa el mode fluid |
end |
Readable | No queden més dades per llegir |
error |
Tots dos | Fallada en l'operació. Si no l'escoltes, tomba el procés |
close |
Tots dos | El descriptor subjacent s'ha tancat i alliberat |
finish |
Writable | S'ha cridat end() i tot el pendent s'ha abocat |
drain |
Writable | La memòria intermèdia interna s'ha buidat: pots tornar a escriure |
Dos advertiments que valen per mitja lliçó. El primer: end i finish no són el mateix. end és de la banda que llegeix («s'ha acabat l'entrada») i finish és de la banda que escriu («he acabat d'abocar»); confondre'ls porta a tancar un fitxer abans que les seves dades siguin al disc. El segon: l'esdeveniment error d'un stream segueix la regla de l'EventEmitter que ja coneixes —un error sense oients llança una excepció no capturada i mata el procés—. Un stream sense on('error') és una caiguda de producció esperant a passar, i el cas més habitual no és un disc trencat: és un ENOENT perquè el fitxer no hi era.
- Mode fluid i mode pausat
Un Readable té dues maneres de lliurar dades, i aquesta dualitat és la font de la meitat dels problemes de qui comença amb streams.
| Mode fluid (flowing) | Mode pausat (paused) | |
|---|---|---|
| Com s'activa | on('data'), pipe() o resume() |
És l'estat inicial; s'hi torna amb pause() |
| Qui marca el ritme | L'stream: empeny les dades | Tu: les demanes amb read() |
| Com es consumeix | Callback de data |
on('readable') + read() |
| Risc | Si trigues a processar, les dades continuen arribant | Oblidar de cridar read() i quedar-te aturat |
// Mode fluid: l'stream empeny.
lector.on('data', (tros) => processar(tros));
// Mode pausat: tu estires.
lector.on('readable', () => {
let tros;
while ((tros = lector.read()) !== null) processar(tros);
});No els barregis. Subscriure's a data i cridar read() al mateix stream produeix comportaments difícils de raonar: trossos que es perden, end que no arriba, ordre alterat. Tria un mode per stream i mantén-lo —encara que a la pràctica la recomanació és no fer-ne servir cap directament: pipe, pipeline o for await...of resolen el problema millor—. I un tercer detall del mode fluid: si et subscrius a data després d'un await, pots perdre trossos, perquè l'stream va començar a fluir abans que arribés el teu oient. Subscriu-t'hi sempre de manera síncrona, just després de crear l'stream.
createReadStream, createWriteStream i highWaterMark
createReadStream, createWriteStream i highWaterMarkconst fs = require('node:fs');
const lector = fs.createReadStream(FITXER_VENDES, {
encoding: 'utf8', // sense aixo, els trossos son Buffer
highWaterMark: 64 * 1024 // mida de la memoria intermedia interna: 64 KB (per defecte)
});
const escriptor = fs.createWriteStream(rutaSortida, { flags: 'a' }); // 'a': afegirEl highWaterMark és la mida de la memòria intermèdia interna: quants bytes acumula l'stream abans de considerar que ja en té prou. Abaixar-lo (16 KB) gasta menys memòria per stream a canvi de més crides al sistema i més esdeveniments; apujar-lo (1 MB) fa el contrari. Els 64 KB per defecte són raonables gairebé sempre: abaixar-lo té sentit quan manegues milers d'streams simultanis —un servidor amb moltes connexions— i apujar-lo quan processes pocs fitxers molt grans. En mode objecte (que veuràs a la lliçó següent) el highWaterMark compta objectes, no bytes, i val 16 per defecte.
Un advertiment sobre encoding: si no l'indiques, els trossos arriben com a Buffer; i si l'indiques com a 'utf8', l'stream s'encarrega que cap caràcter multibyte no quedi partit entre dos trossos —un problema real que veurem a fons a la lliçó 03-06 amb StringDecoder—.
- Contrapressió: el que torna
write()
write()Aquí hi ha el concepte que separa qui fa servir streams de qui els entén. Imagina que llegeixes d'un SSD ràpid i escrius en un disc lent o en una connexió de xarxa: les dades entren a 500 MB/s i surten a 50 MB/s. On va la diferència? A la memòria intermèdia interna de l'stream d'escriptura, que creix sense parar fins a exhaurir la memòria. És a dir: has canviat un readFile que consumia 800 MB per un stream que consumeix... 800 MB.
La contrapressió (backpressure) és el mecanisme que ho impedeix, i es recolza en un detall que gairebé tothom ignora:
const esPotContinuar = escriptor.write(tros);
// true -> la memoria intermedia te lloc, continua escrivint
// false -> la memoria intermedia es PLENA. Deixa d'escriure i espera l'esdeveniment 'drain'write() torna un booleà. I aquest booleà no és informatiu: és una ordre.
// MALAMENT: ignorar el valor de retorn.
lector.on('data', (tros) => {
escriptor.write(tros); // torna false i a ningu no li importa
});Aquest codi funciona amb fitxers petits i peta amb els grans: la memòria intermèdia de l'escriptor creix indefinidament perquè ningú no deixa d'alimentar-la. És la fallada més comuna en escriure streams a mà, i la més difícil de diagnosticar, perquè en desenvolupament —fitxer petit, disc ràpid— no es manifesta mai.
// BE: respectar la contrapressio.
lector.on('data', (tros) => {
if (!escriptor.write(tros)) {
lector.pause(); // deixa de llegir
escriptor.once('drain', () => lector.resume()); // repren en buidar-se
}
});
lector.on('end', () => escriptor.end());El cicle complet és: data → write() torna false → pause() → la memòria intermèdia es buida i l'escriptor emet drain → resume() → tornada a data. El resultat és que el consumidor lent marca el ritme del productor ràpid, i la memòria es manté acotada pel highWaterMark. Aquesta idea no és exclusiva dels fitxers: és la mateixa que governa TCP i la que farà que un client lent no tombi el teu servidor HTTP al Mòdul 4.
pipe() i el seu punt feble
pipe() i el seu punt febleEscriure a mà el ball de pause/resume/drain a cada canonada seria insuportable. pipe() ho fa per tu:
Aquesta línia equival a tot el bloc anterior: connecta els dos streams, gestiona la contrapressió i crida escriptor.end() quan el lector acaba. Es poden encadenar —a.pipe(b).pipe(c)— perquè pipe torna l'stream de destinació.
Però pipe té un defecte greu i poc conegut: no propaga errors ni neteja el que deixa enrere. Si lector falla —un ENOENT, un disc amb errors—, l'error no arriba a escriptor, que queda obert amb el seu descriptor sense alliberar; i si ningú no escolta error a lector, el procés mor.
Amb una canonada de tres o quatre etapes el problema es multiplica: cal subscriure on('error') a cada stream i tancar a mà els que quedin penjant. És tediós, és fàcil oblidar-se'n d'un, i aquest oblit es paga en descriptors perduts fins a l'EMFILE.
Per això la recomanació moderna és rotunda: fes servir pipe() per entendre el concepte i stream.pipeline per treballar. pipeline propaga errors, destrueix tots els streams de la cadena quan alguna cosa falla i té versió amb promeses. És el tema central de la lliçó següent.
- El fitxer
dades/vendes.csv d'Escena Viva
dades/vendes.csv d'Escena VivaNecessitem dades amb què treballar. Aquest és el format de l'històric de vendes:
codiEntrada,sessioId,dataVenda,preuCentims,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
Les columnes segueixen les convencions del projecte: codiEntrada amb el format EV-<any>-<6 dígits>, sessioId amb la forma ses-NNN-M, dataVenda com a cadena ISO sense zona, preuCentims enter i canal amb un de tres valors (web, taquilla, app). Generem un fitxer coherent amb la llavor: exactament 1811 vendes, repartides segons les venudes de cada sessió.
// src/laboratori/generar-vendes.js
// Genera dades/vendes.csv coherent amb dades/esdeveniments.json.
const fs = require('node:fs');
const { obtenirCataleg } = require('../cataleg-dades.js');
const { FITXER_VENDES } = require('../config/rutes.js');
const CANALS = ['web', 'taquilla', 'app'];
async function principal() {
const esdeveniments = await obtenirCataleg();
const escriptor = fs.createWriteStream(FITXER_VENDES, { encoding: 'utf8' });
escriptor.write('codiEntrada,sessioId,dataVenda,preuCentims,canal\n');
let numero = 0;
for (const esdeveniment of esdeveniments) {
for (const sessio of esdeveniment.sessions) {
for (let i = 0; i < sessio.venudes; i += 1) {
numero += 1;
const codi = `EV-2026-${String(numero).padStart(6, '0')}`;
const canal = CANALS[numero % CANALS.length];
// Vendes repartides per minuts des de l'obertura de taquilla.
const data = new Date(2026, 5, 14, 10, numero % 600).toISOString().slice(0, 19);
// La contrapressio s'ignora aqui expressament: son 1811 linies.
escriptor.write(`${codi},${sessio.id},${data},${sessio.preuCentims},${canal}\n`);
}
}
}
escriptor.end();
// 'finish' arriba quan TOT s'ha abocat al disc, no en acabar end().
escriptor.on('finish', () => console.error(`[vendes] ${numero} vendes escrites`));
}
if (require.main === module) principal();node src/laboratori/generar-vendes.js
# [vendes] 1811 vendes escrites
wc -l dades/vendes.csv
# 1812 dades/vendes.csv (1811 vendes + capcalera)El comentari sobre la contrapressió és deliberat: amb 1811 línies no hi ha problema, però el codi correcte per a volums reals hauria de respectar el retorn de write() o, millor, construir-se amb pipeline i un generador, com farem a la lliçó següent.
- Llegir línia a línia amb
readline
readlineUn CSV es processa per línies, però els trossos d'un stream no coincideixen amb les línies: un tros de 64 KB talla per la meitat la línia que li toqui. Al laboratori de l'apartat 1 ho vam resoldre a mà amb una variable resta; el mòdul node:readline ho fa bé i sense codi propi.
// src/informes/vendes-per-sessio.js
// Recorre dades/vendes.csv linia a linia i acumula el total per sessio.
const fs = require('node:fs');
const readline = require('node:readline');
const { FITXER_VENDES } = require('../config/rutes.js');
async function totalitzarPerSessio(ruta = FITXER_VENDES) {
const lector = readline.createInterface({
input: fs.createReadStream(ruta, { encoding: 'utf8' }),
crlfDelay: Infinity // tracta \r\n com un sol salt (fitxers de Windows)
});
const perSessio = new Map();
let numeroLinia = 0;
for await (const linia of lector) {
numeroLinia += 1;
if (numeroLinia === 1 || linia.trim() === '') continue; // capcalera i blancs
const [, sessioId, , preuCentims, canal] = linia.split(',');
if (!sessioId || Number.isNaN(Number(preuCentims))) {
// Diagnostic per stderr: una linia corrupta no avorta l'informe.
console.error(`[vendes] linia ${numeroLinia} ignorada: ${linia}`);
continue;
}
const acumulat = perSessio.get(sessioId) ??
{ sessioId, entrades: 0, recaptacioCentims: 0, canals: new Set() };
acumulat.entrades += 1;
acumulat.recaptacioCentims += Number(preuCentims);
acumulat.canals.add(canal);
perSessio.set(sessioId, acumulat);
}
return [...perSessio.values()]
.map((a) => ({ ...a, canals: [...a.canals].join('/') }))
.sort((a, b) => a.sessioId.localeCompare(b.sessioId));
}
module.exports = { totalitzarPerSessio };node -e "require('./src/informes/vendes-per-sessio.js').totalitzarPerSessio().then(console.table)"
# ses-001-1 180 entrades 450000 centims web/taquilla/app
# ses-001-2 96 entrades 211200 centims web/taquilla/app
# ses-002-1 118 entrades 212400 centims web/taquilla/app
# ...Tres decisions dignes d'esment. crlfDelay: Infinity evita que un fitxer generat a Windows produeixi línies acabades en \r invisible que espatlla l'últim camp. Una línia corrupta es registra i se salta, no avorta l'informe: en un històric de milions de registres sempre hi ha brossa, i un informe que mor a la línia 4.000.000 no serveix de res. I el consum de memòria és constant: perSessio guarda una entrada per sessió (set), no per venda.
for await...of sobre un stream
for await...of sobre un streamAquest for await (const linia of lector) mereix explicació, perquè és la manera més llegible de consumir un stream i ja la coneixes de la lliçó 02-05, quan vas veure events.on().
Tot Readable és un iterable asíncron. Es pot recórrer directament:
// Consum amb for await: sense callbacks, sense pause/resume manual.
const lector = fs.createReadStream(FITXER_VENDES, { encoding: 'utf8' });
let bytes = 0;
for await (const tros of lector) {
bytes += tros.length;
}
console.log(`${bytes} caracters llegits`);Els seus avantatges sobre on('data') són concrets: la contrapressió és automàtica (el bucle no demana el tros següent fins que acaba el cos) en comptes de manual; els errors es capturen amb un try/catch normal al voltant del bucle en comptes d'amb un on('error') a part; es pot fer servir await a dins; i un break destrueix l'stream tot sol, sense destroy() explícit.
Aquest await dins del bucle és la diferència decisiva. Si per cada línia del CSV cal consultar una base de dades, amb on('data') les consultes es llançarien totes alhora i enfonsarien el servidor; amb for await, la lectura s'atura sola fins que la consulta acaba, així que la contrapressió surt de franc. El preu és rendiment brut: for await és una mica més lent perquè cada iteració crea una promesa. Per a un procés per lots és irrellevant; en una ruta crítica amb molt trànsit, mesura-ho.
Errors Comuns i Consells
- No subscriure
on('error'). Unerrorsense oients en unEventEmittermata el procés, i un stream és unEventEmitter. La solució definitiva éspipeline. - Ignorar el retorn de
write(). Funciona en desenvolupament i exhaureix la memòria en producció. Si escrius a mà, respecta eldrain. - Confondre
endambfinish.endés el lector exhaurit;finishés l'escriptor buidat. Tancar per l'esdeveniment equivocat talla dades. - Suposar que un tros és una línia. Mai no ho és. Fes servir
readlineo unTransformque trossegi. - Barrejar mode fluid i pausat, o subscriure's a
datadesprés d'unawait. En el primer cas el comportament és erràtic; en el segon perds trossos. - Acumular tots els trossos en un array per ajuntar-los al final. Això és
readFileamb passos extra: has perdut l'únic avantatge de l'stream. - Consell: si el fitxer cap folgadament a la memòria i només el llegeixes un cop,
readFileés més simple i més ràpid; els streams són per al que és gran, infinit o el que arriba a poc a poc. I quan dubtis de si hi ha contrapressió, imprimeixescriptor.writableLengthde tant en tant: si creix sense parar, l'estàs ignorant.
Exercicis
Exercici 1: informe de vendes per canal i per hora
Amplia src/informes/vendes-per-sessio.js amb una funció resumirPerCanal() que recorri dades/vendes.csv una sola vegada i torni, per canal, el nombre d'entrades, la recaptació total i l'hora punta (l'hora del dia amb més vendes). Ha de fer servir readline, mantenir memòria constant i validar que la suma d'entrades de tots els canals és 1811.
Exercici 2: còpia amb contrapressió manual
Escriu src/laboratori/copiar-amb-contrapressio.js que copiï un fitxer gran sense fer servir pipe, respectant el drain, i imprimeixi cada segon: megabytes copiats, writableLength de l'escriptor i nombre de pauses per contrapressió. Executa després una versió que ignori el retorn de write() i compara el consum de memòria amb process.memoryUsage().rss.
Exercici 3: filtrar vendes d'una sala
Escriu src/laboratori/filtrar-vendes.js que llegeixi dades/vendes.csv, es quedi només amb les vendes les sessions de les quals pertanyin a una sala donada (--sala="Sala Boveda", creuant amb el catàleg) i escrigui el resultat a dades/vendes-<sala>.csv conservant la capçalera. Fes servir readline per llegir i un createWriteStream per escriure, respectant la contrapressió.
Solucions
Solució 1. La clau és acumular en dos nivells durant el mateix recorregut:
for await (const linia of lector) {
// ...saltar capcalera i linies buides com abans...
const [, , dataVenda, preuCentims, canal] = linia.split(',');
const acumulat = perCanal.get(canal) ??
{ canal, entrades: 0, recaptacioCentims: 0, perHora: new Array(24).fill(0) };
acumulat.entrades += 1;
acumulat.recaptacioCentims += Number(preuCentims);
// 'AAAA-MM-DDTHH:mm:ss' -> l'hora es a les posicions 11 i 12.
acumulat.perHora[Number(dataVenda.slice(11, 13))] += 1;
perCanal.set(canal, acumulat);
}
return [...perCanal.values()].map(({ perHora, ...resta }) => ({
...resta,
horaPunta: perHora.indexOf(Math.max(...perHora))
}));L'array de 24 posicions per canal és el truc que manté la memòria constant: no es guarden les vendes, només el comptador de cada hora. Un Map de dates completes creixeria amb el fitxer.
Solució 2. El nucli de la còpia manual:
lector.on('data', (tros) => {
copiats += tros.length;
if (!escriptor.write(tros)) {
pauses += 1;
lector.pause();
escriptor.once('drain', () => lector.resume());
}
});
lector.on('end', () => escriptor.end());
lector.on('error', (e) => { console.error(e); escriptor.destroy(); });
escriptor.on('error', (e) => { console.error(e); lector.destroy(); });Amb la contrapressió respectada, writableLength oscil·la al voltant del highWaterMark i el rss es manté pla. Sense ella, writableLength creix sense límit i el rss puja fins a acostar-se a la mida del fitxer. Fixa't també en els dos on('error') creuats que calen per no perdre descriptors: aquesta feina manual és exactament el que pipeline automatitza.
Solució 3. El que importa és creuar el catàleg abans de començar a llegir el CSV, per no fer-ho per línia:
const esdeveniments = await obtenirCataleg();
const sessionsDeLaSala = new Set(
esdeveniments.filter((e) => e.sala === sala).flatMap((e) => e.sessions.map((s) => s.id))
);
const escriptor = fs.createWriteStream(rutaSortida, { encoding: 'utf8' });
let primera = true;
for await (const linia of lector) {
if (primera) { escriptor.write(`${linia}\n`); primera = false; continue; }
if (!sessionsDeLaSala.has(linia.split(',')[1])) continue;
// for await ja aplica contrapressio a la LECTURA; aqui l'apliquem
// a l'ESCRIPTURA esperant el drain quan la memoria intermedia s'omple.
if (!escriptor.write(`${linia}\n`)) {
await new Promise((resoldre) => escriptor.once('drain', resoldre));
}
}
escriptor.end();Aquest await new Promise(... 'drain' ...) és la traducció de la contrapressió al món d'async/await, i és un patró que val la pena memoritzar: dins d'un for await, esperar el drain atura també la lectura, perquè el bucle no demana l'element següent fins que acaba el cos. Un Set amb els ids de sessió evita recórrer el catàleg 1811 vegades.
Conclusió
Ja saps per què existeixen els streams i quin problema resolen exactament: has mesurat la diferència entre 1621 MB de readFile i 6 MB d'un stream sobre el mateix fitxer, i entens que el consum d'un stream no depèn de la mida de l'entrada. Coneixes els quatre tipus —Readable, Writable, Duplex i Transform— i el fet fonamental que són EventEmitter, amb data, end, error, close, finish i drain, sense confondre end amb finish i sabent que un error sense oients tomba el procés.
Distingeixes el mode fluid del pausat i per què no s'han de barrejar; saps què és el highWaterMark i què es guanya i es perd movent-lo. I, sobretot, entens la contrapressió: que write() torna un booleà que és una ordre, que ignorar-lo converteix el teu stream en un readFile disfressat, i que el cicle pause → drain → resume és el que fa que el consumidor lent marqui el ritme. Saps que pipe() automatitza aquest ball però no propaga errors ni neteja descriptors, i per això no és l'eina definitiva. Escena Viva, mentrestant, ja té el seu dades/vendes.csv amb les 1811 vendes de la llavor i un src/informes/vendes-per-sessio.js que el recorre línia a línia amb readline, acumulant entrades, recaptació i canals per sessió amb memòria constant. I has vist la manera més llegible de consumir qualsevol stream: for await...of, que porta la contrapressió de regal i permet await dins del bucle.
A Streams de Transformació i pipeline tanquem el cercle. Aprendràs a escriure els teus propis streams amb Transform (_transform i _flush), a muntar canonades de procés completes —CSV cru a objectes, filtratge per sala, agregació per sessió, escriptura de l'informe—, i a substituir pipe() per stream.pipeline, que propaga errors, destrueix tota la cadena quan alguna cosa falla i té versió amb promeses. Veuràs a més Readable.from(), els generadors asíncrons com a transformacions, la compressió amb zlib intercalada a la canonada i la cancel·lació amb AbortSignal.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
