Portem dos mòduls amb un deute pendent. dades/esdeveniments.json existeix des de la primera lliçó, té els tres esdeveniments i les set sessions d'Escena Viva, i l'aplicació no l'ha llegit mai: src/cataleg-dades.js continua tornant un array incrustat dins del codi mateix. Avui saldem aquest deute.

El mòdul fs (file system) és la porta de Node al disc. És també el lloc on tot el del Mòdul 2 deixa de ser teoria: fs.promises són promeses de les que ja coneixes, readFileSync és exactament el tipus d'operació que congela el bucle d'esdeveniments, i triar entre l'una i l'altra és la diferència entre un servidor que respon a mil usuaris i un que es queda mut mentre llegeix un fitxer.

En acabar, obtenirCataleg() serà una funció asíncrona que llegeix JSON del disc, sabràs per què l'asincronia es propaga cap amunt, distingiràs els errors del sistema de fitxers pel seu codi, i podràs escriure un fitxer sense risc de deixar-lo a mitges si el procés mor enmig de l'operació.

Contingut

  1. Les tres APIs del mòdul fs
  2. readFile, la codificació i què passa si l'omets
  3. Quan és acceptable la versió síncrona (i quan és un desastre)
  4. La feina central: una capa de dades asíncrona
  5. L'asincronia es propaga cap amunt
  6. Els errors del sistema de fitxers i els seus codis
  7. Per què comprovar abans amb exists és una mala idea
  8. Escriure: writeFile, appendFile i l'opció flag
  9. Escriptura atòmica: fitxer temporal i rename
  10. Memoritzar el catàleg per no rellegir-lo

  1. Les tres APIs del mòdul fs

Node ofereix tres maneres diferents de fer el mateix amb fitxers. No és una redundància històrica accidental: cadascuna té el seu moment.

// 1. Promeses (la que farem servir a tot el curs)
const contingut = await require('node:fs/promises').readFile('dades/esdeveniments.json', 'utf8');

// 2. Callbacks error-first (l'API original de Node)
require('node:fs').readFile('dades/esdeveniments.json', 'utf8', (error, contingut) => { /* ... */ });

// 3. Sincrona (bloqueja el fil principal)
const contingut = require('node:fs').readFileSync('dades/esdeveniments.json', 'utf8');
API Com s'importa Bloqueja el bucle? Gestió d'errors Quan fer-la servir
Promeses require('node:fs/promises') No try/catch amb await Per defecte, sempre
Callbacks require('node:fs') No Primer argument del callback Codi antic; API que exigeix callback
Síncrona require('node:fs'), sufix Sync Sí try/catch directe Només a l'arrencada i en scripts d'un sol ús

Dos detalls: node:fs/promises i node:fs són mòduls diferents (també arribes al primer amb require('node:fs').promises, però importar el submòdul és més explícit), i les funcions amb sufix Sync només existeixen a node:fs: a l'API de promeses no n'hi ha cap, per disseny.

  1. readFile, la codificació i què passa si l'omets

readFile obre el fitxer, el llegeix sencer en memòria i el tanca. Aquesta paraula —sencer— és la seva característica definitòria i el seu límit: un fitxer de 800 MB ocupa 800 MB de memòria. Per a això hi ha els streams de la lliçó 03-04; per a un JSON d'uns quants kilobytes, readFile és l'eina correcta.

const ambCodificacio = await fs.readFile('dades/esdeveniments.json', 'utf8');
console.log(typeof ambCodificacio);               // 'string'

const senseCodificacio = await fs.readFile('dades/esdeveniments.json');
console.log(Buffer.isBuffer(senseCodificacio));   // true
console.log(senseCodificacio.slice(0, 12));
// <Buffer 5b 0a 20 20 7b 0a 20 20 20 20 22 69>

Sense codificació, readFile torna un Buffer: una seqüència de bytes crus. Amb codificació, Node descodifica aquests bytes a text i et dona una cadena.

La raó és que un fitxer no conté text: conté bytes. Que aquests bytes signifiquin "Concierto" o els píxels d'un cartell depèn de com els interpretis. 'utf8' li diu a Node: aquests bytes són text codificat en UTF-8, converteix-los. Els Buffer són el tema complet de la lliçó 03-06; de moment n'hi ha prou amb la regla: 'utf8' per a JSON, CSV, configuració o qualsevol text; sense codificació per a imatges, PDF, ZIP o qualsevol binari.

Un advertiment sobre la ruta: 'dades/esdeveniments.json' és relativa al directori des del qual executes el procés, no al fitxer que conté aquesta línia. És una de les fonts d'error més habituals a Node i la desmuntarem a la lliçó 03-03; de moment, executa sempre des de l'arrel del projecte.

  1. Quan és acceptable la versió síncrona (i quan és un desastre)

readFileSync no és «la versió fàcil». És una operació que atura el fil principal del tot: durant aquest temps el bucle d'esdeveniments no avança, no s'atén cap petició i no s'executa cap temporitzador. Anem a mesurar-ho amb el mesurador del Mòdul 2.

// src/laboratori/comparar-bloqueig.js
// Demostra l'efecte de readFileSync sobre el bucle d'esdeveniments.

const fs = require('node:fs');
const fsPromeses = require('node:fs/promises');
const { iniciarMesura } = require('../utils/mesurar-bucle.js');

const RUTA = 'dades/esdeveniments.json';
const REPETICIONS = 400;

async function mesurar(etiqueta, tasca) {
  const inici = process.hrtime.bigint();
  await tasca();
  console.log(`${etiqueta}: ${(Number(process.hrtime.bigint() - inici) / 1e6).toFixed(1)} ms`);
}

async function principal() {
  // El mesurador avisa per stderr cada cop que el bucle es retarda mes de 20 ms.
  const aturar = iniciarMesura({ intervalMs: 20, llindarMs: 20 });

  await mesurar('sincrona ', () => {
    for (let i = 0; i < REPETICIONS; i += 1) fs.readFileSync(RUTA, 'utf8');
  });
  await mesurar('asincrona', async () => {
    for (let i = 0; i < REPETICIONS; i += 1) await fsPromeses.readFile(RUTA, 'utf8');
  });

  aturar();
}

if (require.main === module) principal();
node src/laboratori/comparar-bloqueig.js
# [bucle] retard de 31.4 ms
# sincrona : 38.2 ms
# asincrona: 61.7 ms

Els dos resultats sorprenen i tots dos importen. La versió síncrona és més ràpida en total: no cal programar callbacks, ni passar pel thread pool, ni tornar al bucle d'esdeveniments. I alhora va bloquejar el bucle 31 ms, mentre que l'asíncrona no el va bloquejar ni una sola vegada; durant aquests 31 ms el teu servidor estava mort per a tothom. Aquí hi ha la clau que molta gent no acaba d'entendre: l'asincronia no és més ràpida, és més justa. No optimitza la feina d'un usuari, permet atendre els altres mentre es fa. Amb un sol usuari, Sync guanya; amb cinc-cents, Sync és una catàstrofe.

Context Sync acceptable? Motiu
Arrencada del procés, abans d'escoltar peticions Sí Encara no hi ha ningú esperant; 30 ms d'arrencada no molesten
Script de línia d'ordres d'un sol ús Sí No hi ha concurrència a protegir
Dins d'una petició HTTP Mai Congela tots els usuaris connectats, no només el que demana
Dins d'un bucle sobre molts fitxers Mai El bloqueig es multiplica pel nombre de fitxers

Regla operativa: si el procés ja està servint trànsit, Sync està prohibit.

  1. La feina central: una capa de dades asíncrona

Ha arribat el moment. src/cataleg-dades.js es va escriure al Mòdul 2 amb les dades incrustades i amb una signatura dissenyada precisament per a aquest canvi. El substituïm sencer:

// src/cataleg-dades.js
// Capa d'acces a les dades del cataleg d'Escena Viva.
// Llegeix la llavor dades/esdeveniments.json i torna objectes de domini.

const fs = require('node:fs/promises');

const { Esdeveniment } = require('./domini');

const RUTA_ESDEVENIMENTS = 'dades/esdeveniments.json';

// Crea un error de domini conservant la causa original.
function fallada(codi, missatge, causa) {
  const error = new Error(missatge);
  error.codi = codi;
  error.cause = causa;
  return error;
}

// Llegeix el fitxer llavor i torna les dades planes ja analitzades.
async function llegirFitxerEsdeveniments() {
  let contingut;

  try {
    contingut = await fs.readFile(RUTA_ESDEVENIMENTS, 'utf8');
  } catch (error) {
    if (error.code !== 'ENOENT') throw error;
    throw fallada('DADES_NO_DISPONIBLES', `No es troba ${RUTA_ESDEVENIMENTS}`, error);
  }

  try {
    return JSON.parse(contingut);
  } catch (error) {
    // JSON.parse llanca SyntaxError: el traduim al vocabulari del domini.
    throw fallada('DADES_CORRUPTES', `${RUTA_ESDEVENIMENTS} no conte JSON valid`, error);
  }
}

// Torna el cataleg complet com a instancies d'Esdeveniment.
async function obtenirCataleg() {
  const dades = await llegirFitxerEsdeveniments();
  return dades.map((registre) => Esdeveniment.desDeJSON(registre));
}

// Torna un unic esdeveniment pel seu id, o undefined si no existeix.
async function obtenirEsdevenimentPerId(id) {
  return (await obtenirCataleg()).find((esdeveniment) => esdeveniment.id === id);
}

module.exports = { obtenirCataleg, obtenirEsdevenimentPerId, RUTA_ESDEVENIMENTS };

Quatre decisions que val la pena assenyalar. Ja no cal structuredClone: abans copiàvem perquè l'array vivia a la memòria cau de mòduls i el compartia tot el procés; ara cada crida llegeix el fitxer i construeix instàncies noves, així que l'aïllament surt gratis. La capa torna objectes de domini, no dades planes: Esdeveniment.desDeJSON feia des del Mòdul 1 que esperava aquest moment, i qui consumeix el catàleg rep Esdeveniment amb els seus getters, no diccionaris anònims. Els errors es tradueixen al vocabulari del domini amb error.codi, perquè qui crida no hauria d'haver de conèixer ENOENT ni SyntaxError. I cause conserva l'error original: és estàndard des de Node 16 i console.error l'imprimeix automàticament, de manera que no perds la traça mentre dones un missatge llegible.

  1. L'asincronia es propaga cap amunt

Ara obtenirCataleg() torna una promesa. Aquest canvi no es queda al mòdul: puja per tota la cadena de crides, i src/cataleg.js s'hi ha d'adaptar.

// src/cataleg.js (fragment: nomes canvia principal)

async function principal() {
  const opcions = llegirOpcions(process.argv.slice(2));

  let esdeveniments;
  try {
    // L'unic canvi real: un await. Ja no cal el .map de conversio,
    // perque la capa de dades torna instancies d'Esdeveniment.
    esdeveniments = await obtenirCataleg();
  } catch (error) {
    console.error(`[cataleg] ${error.message}`);
    if (error.cause) console.error(`[cataleg] causa: ${error.cause.message}`);
    process.exitCode = 1;
    return;
  }

  // ...la resta (filtres, mostrarTaulaResum, mostrarCataleg, totals) queda igual.
}

if (require.main === module) {
  // principal() ara torna una promesa: cal capturar-ne el rebuig.
  principal().catch((error) => {
    console.error('[cataleg] error inesperat:', error);
    process.exitCode = 1;
  });
}

L'asincronia és contagiosa cap amunt: si una funció espera alguna cosa asíncrona, ella mateixa es torna asíncrona, i qui la cridi també. La cadena readFile → llegirFitxerEsdeveniments → obtenirCataleg → principal acaba en un punt anomenat frontera, on algú ha de decidir què fa amb l'error. En un script de consola aquesta frontera és el require.main === module; al Mòdul 4 serà el gestor de la petició HTTP. El que no ha de passar mai és cridar principal() sense .catch(): seria una promesa rebutjada sense gestionar i, com vas veure al Mòdul 2, això tomba el procés amb un unhandledRejection.

  1. Els errors del sistema de fitxers i els seus codis

Els errors de fs porten una propietat code amb un identificador estable. No comparis mai el missatge de text: canvia entre versions i idiomes del sistema.

code Significat Causa habitual
ENOENT No such file or directory Ruta mal escrita, fitxer esborrat, directori pare inexistent
EACCES Permission denied L'usuari del procés no té permís de lectura o escriptura
EISDIR / ENOTDIR És un directori / un tram no ho és Llegir una carpeta com a fitxer; esdeveniments.json/altre.txt
EEXIST El fitxer ja existeix Escriptura amb flag: 'wx'
EMFILE Massa fitxers oberts Descriptors sense tancar; obrir-ne milers alhora
ENOSPC No queda espai al disc Disc ple en escriure
EPERM Operació no permesa Fitxer bloquejat (típic a Windows)

L'error porta a més error.path (la ruta implicada), error.syscall (open, read, unlink) i error.errno. La distinció operativa és aquesta: ENOENT sol ser un cas previst —l'informe d'avui encara no existeix— i mereix una branca del codi; els altres són fallades reals que cal propagar. Capturar-los tots per igual és la manera més ràpida d'amagar un EACCES de producció darrere d'un «no hi havia dades».

  1. Per què comprovar abans amb exists és una mala idea

Sembla de sentit comú escriure això:

// MALAMENT: comprovar abans d'actuar.
async function llegirSiExisteix(ruta) {
  try {
    await fs.access(ruta);              // existeix?
  } catch {
    return null;                        // no existeix
  }
  return fs.readFile(ruta, 'utf8');     // existeix, el llegeixo
}

És incorrecte per un motiu profund. Entre la línia de l'access i la del readFile hi ha una finestra de temps en què el bucle d'esdeveniments fa altres coses, i en aquesta finestra un altre procés —o el teu propi programa— pot esborrar el fitxer, reanomenar-lo o treure-li permisos. Quan arriba el readFile, la comprovació ja és mentida. És la clàssica condició de cursa TOCTOU (Time Of Check to Time Of Use): comproves en un instant i uses en un altre. A més d'incorrecte, duplica la feina: dues crides al sistema on n'hi havia prou amb una.

// BE: intentar i capturar.
async function llegirSiExisteix(ruta) {
  try {
    return await fs.readFile(ruta, 'utf8');
  } catch (error) {
    if (error.code === 'ENOENT') return null;  // cas previst
    throw error;                                // fallada real
  }
}

L'operació de lectura ja comprova l'existència, de manera atòmica i dins del sistema operatiu. La regla general: intenta i captura, no preguntis i actuïs. Per això fs.exists està obsolet des de fa anys (el seu callback ni tan sols seguia el conveni error-first); fs.existsSync encara existeix i és legítim a l'arrencada d'un script per donar un missatge clar, però mai com a pas previ a una operació.

  1. Escriure: writeFile, appendFile i l'opció flag

// Escriu el fitxer sencer. Si existeix, el TRUNCA i el substitueix.
await fs.writeFile('informes/ocupacio.json', JSON.stringify(dades, null, 2), 'utf8');

// Afegeix al final. Si no existeix, el crea.
await fs.appendFile('informes/auditoria.log', `${new Date().toISOString()} venda\n`, 'utf8');

Totes dues accepten una cadena o un Buffer. Si els passes un objecte sense serialitzar obtindràs el literal [object Object] al fitxer: la serialització sempre és explícita, amb JSON.stringify(x, null, 2) segons la convenció del projecte. Al darrere de totes dues hi ha l'opció flag, que decideix com s'obre el fitxer:

flag Ús Si no existeix Si existeix
'w' Escriptura (per defecte a writeFile) El crea El buida
'a' Afegir (per defecte a appendFile) El crea Escriu al final
'wx' Escriptura exclusiva El crea Falla amb EEXIST
'ax' Afegir exclusiu El crea Falla amb EEXIST
'r' Només lectura Falla amb ENOENT L'obre
'r+' Lectura i escriptura Falla amb ENOENT L'obre sense buidar-lo

'wx' mereix una atenció especial: és la manera correcta de dir «crea aquest fitxer només si no existeix» sense condicions de cursa, perquè l'exclusivitat la garanteix el sistema operatiu en una sola crida. És el principi de l'apartat anterior aplicat a l'escriptura —intentar i capturar EEXIST, no comprovar abans—, i el farem servir a la lliçó següent per no sobreescriure l'informe del dia.

  1. Escriptura atòmica: fitxer temporal i rename

writeFile no és atòmica. Primer trunca el fitxer a zero bytes i després escriu el contingut nou. Si el procés mor entre els dos passos —un Ctrl+C, una fallada elèctrica, l'OOM killer—, et quedes amb dades/esdeveniments.json buit o tallat per la meitat. Has perdut el catàleg.

La solució estàndard es recolza en una garantia del sistema de fitxers: rename sobre el mateix sistema de fitxers és atòmic. Un instant abans hi ha el fitxer vell, un instant després el nou, sense cap moment intermedi observable.

// src/utils/escriptura-atomica.js
// Escriu un fitxer de manera que mai quedi a mitges.

const fs = require('node:fs/promises');

async function escriureAtomic(ruta, contingut) {
  // El temporal va al MATEIX directori: rename nomes es atomic
  // dins del mateix sistema de fitxers.
  const rutaTemporal = `${ruta}.${process.pid}.tmp`;

  try {
    await fs.writeFile(rutaTemporal, contingut, 'utf8');
    await fs.rename(rutaTemporal, ruta);        // pas atomic
  } catch (error) {
    await fs.rm(rutaTemporal, { force: true }); // no deixem brossa pel disc
    throw error;
  }
}

// Guarda el cataleg modificat sense posar en risc l'original.
// toJSON() d'Esdeveniment torna el registre pla equivalent a la llavor.
async function guardarCataleg(ruta, esdeveniments) {
  await escriureAtomic(ruta, JSON.stringify(esdeveniments.map((e) => e.toJSON()), null, 2));
}

module.exports = { escriureAtomic, guardarCataleg };

Per què funciona: mentre s'escriu el temporal, dades/esdeveniments.json continua intacte, de manera que qualsevol lector concurrent veu la versió anterior completa i vàlida; el rename substitueix el nom al directori d'una sola vegada, sense finestra de fitxer truncat; si el procés mor a mitges, el pitjor que queda és un .tmp orfe; i el pid al nom evita que dos processos simultanis es trepitgin el temporal. Per a dades crítiques de debò hi ha un pas més —forçar l'abocament de la memòria cau del sistema al disc amb sync() abans de reanomenar—, però això requereix l'API FileHandle de la lliçó següent.

  1. Memoritzar el catàleg per no rellegir-lo

La nostra obtenirCataleg() llegeix el fitxer a cada crida. Per a un script de consola tant li fa; per a un servidor que atén cent peticions per segon són cent lectures de disc d'un fitxer que no canvia. La solució és la memorització: llegir un cop, guardar el resultat i tornar-lo a les crides següents.

// src/cataleg-dades.js (fragment afegit)

let promesaCataleg = null;   // Cache: guarda la PROMESA, no el resultat.

async function obtenirCataleg({ recarregar = false } = {}) {
  if (recarregar) promesaCataleg = null;

  if (promesaCataleg === null) {
    // Guardem la promesa immediatament, sense esperar-la.
    promesaCataleg = llegirFitxerEsdeveniments()
      .then((dades) => dades.map((registre) => Esdeveniment.desDeJSON(registre)))
      .catch((error) => {
        // Una fallada no ha de quedar en cache per sempre: es neteja i es propaga.
        promesaCataleg = null;
        throw error;
      });
  }

  return promesaCataleg;
}

El detall fi —el que separa una memòria cau correcta d'una d'inútil— és que posem en memòria cau la promesa, no el valor resolt. Si guardéssim el resultat (if (cataleg === null) { cataleg = await llegir(); }), deu crides simultànies durant el primer await veurien totes cataleg === null i llançarien deu lectures de disc en paral·lel: l'anomenada «estampida de memòria cau». En guardar la promesa de manera síncrona, abans de cap await, la segona crida ja troba la promesa en curs i s'hi enganxa. Una sola lectura, encara que arribin mil peticions alhora.

Un efecte secundari que convé conèixer: com que cada Esdeveniment ara es comparteix entre tots els consumidors, si algú hi ven entrades el canvi es veu des de tot arreu. És el que volem fins que arribi la base de dades del Mòdul 7, però també significa que un consumidor pot modificar el que veu un altre.

Errors Comuns i Consells

  • Fer servir readFileSync «perquè és més simple» dins d'un servidor. És la causa número u de latències inexplicables a Node: simple per a tu, catastròfic per als teus usuaris.
  • Oblidar 'utf8' i sorprendre's amb <Buffer 5b 0a ...>. Si esperaves text i veus això, et falta la codificació. I no comparis error.message: el missatge és per a humans, el contracte és error.code.
  • Un try/catch gegant al voltant de tot. Embolcalla l'operació concreta que pot fallar de manera prevista, per distingir un ENOENT esperat d'un error de programació.
  • fs.writeFile(ruta, objecte) escriu [object Object]; i escriure en un directori inexistent falla amb ENOENT, perquè writeFile no crea directoris (això és mkdir amb recursive, a la lliçó següent).
  • Consell: quan un error de fs et desconcerti, imprimeix error.code, error.syscall i error.path; aquests tres camps resolen gairebé qualsevol misteri. I valida les dades tot just llegir-les: un JSON sintàcticament correcte pot portar aforament: "420" en text i trencar els càlculs molt més endavant, en un lloc que no hi té res a veure.

Exercicis

Exercici 1: verificador de la llavor

Escriu src/laboratori/verificar-llavor.js que llegeixi dades/esdeveniments.json amb fs.promises i comprovi: que el fitxer existeixi i sigui JSON vàlid (distingint totes dues fallades amb error.codi); que hi hagi 3 esdeveniments i 7 sessions; que tots els id siguin únics; que cap sessió no tingui venudes > aforament; i que els totals siguin els coneguts (aforament 3000, venudes 1811, lliures 1189). Ha de sortir amb process.exitCode = 1 si alguna cosa falla, imprimir diagnòstics per stderr i el resum per stdout.

Exercici 2: pujada de preus amb escriptura atòmica

Escriu src/laboratori/pujar-preus.js que llegeixi el catàleg amb obtenirCataleg(), pugi un --percentatge=10 el preuCentims de totes les sessions d'una --sala="Sala Boveda" arrodonint a cèntims sencers, guardi el resultat amb escriureAtomic només si es passa --confirmar (sense aquesta opció, únicament mostra els canvis) i afegeixi una línia per sessió modificada a informes/canvis-preu.log amb appendFile.

Exercici 3: mesurar l'estampida de memòria cau

Modifica obtenirCataleg per comptar quantes vegades es llegeix realment el fitxer. Escriu dues versions de la memorització —una que posi en memòria cau el valor i una altra que hi posi la promesa— i llança 50 crides simultànies amb Promise.all sobre cadascuna. Mostra el nombre de lectures de cada versió i explica la diferència.

Solucions

Solució 1. La càrrega és literalment la llegirFitxerEsdeveniments() de l'apartat 4, amb els seus dos try/catch separats i els seus dos error.codi (DADES_NO_DISPONIBLES i DADES_CORRUPTES): reutilitza-la en comptes de duplicar-la. El que és nou és la verificació:

// src/laboratori/verificar-llavor.js (fragment central)
function verificar(dades) {
  const problemes = [];
  const ids = new Set();
  let sessions = 0, aforament = 0, venudes = 0;

  for (const esdeveniment of dades) {
    if (ids.has(esdeveniment.id)) problemes.push(`id repetit: ${esdeveniment.id}`);
    ids.add(esdeveniment.id);

    for (const sessio of esdeveniment.sessions) {
      if (ids.has(sessio.id)) problemes.push(`id repetit: ${sessio.id}`);
      ids.add(sessio.id);
      if (sessio.venudes > sessio.aforament) {
        problemes.push(`${sessio.id}: venudes ${sessio.venudes} > aforament ${sessio.aforament}`);
      }
      sessions += 1;
      aforament += sessio.aforament;
      venudes += sessio.venudes;
    }
  }

  if (sessions !== 7) problemes.push(`s'esperaven 7 sessions, n'hi ha ${sessions}`);
  if (aforament !== 3000) problemes.push(`aforament ${aforament}, s'esperava 3000`);
  if (venudes !== 1811) problemes.push(`venudes ${venudes}, s'esperaven 1811`);

  return { problemes, sessions, aforament, venudes, lliures: aforament - venudes };
}

Un sol Set serveix per a esdeveniments i sessions perquè els prefixos evt- i ses- ja els mantenen en espais diferents. I fixa't que s'acumulen els problemes en comptes d'abortar al primer: un verificador que només explica la primera fallada obliga a executar-lo cinc vegades. principal() imprimeix el resum per stdout, aboca els problemes per stderr i ajusta process.exitCode.

Solució 2. L'estructura és la de cataleg.js: llegir opcions, transformar, decidir. El que és interessant és que el mode simulació és el predeterminat.

const esdeveniments = await obtenirCataleg();
const canvis = [];

for (const esdeveniment of esdeveniments.filter((e) => e.sala === sala)) {
  for (const sessio of esdeveniment.sessions) {
    const anterior = sessio.preuCentims;
    sessio.preuCentims = Math.round(anterior * (1 + percentatge / 100));
    canvis.push({ sessio: sessio.id, anterior, nou: sessio.preuCentims });
  }
}

console.table(canvis);

if (!confirmar) {
  console.error('Simulacio. Afegeix --confirmar per escriure els canvis.');
  return;
}

await guardarCataleg(RUTA_ESDEVENIMENTS, esdeveniments);
await fs.appendFile('informes/canvis-preu.log', canvis
  .map((c) => `${new Date().toISOString()} ${c.sessio} ${c.anterior} -> ${c.nou}\n`)
  .join(''), 'utf8');

Que un script que modifica dades exigeixi confirmació explícita no és un caprici: és la diferència entre equivocar-se i poder desfer-ho. Compte amb l'appendFile: si informes/ no existeix falla amb ENOENT, perquè escriure no crea directoris; crea'l a mà amb mkdir informes fins a la lliçó següent.

Solució 3. La versió que posa en memòria cau el valor imprimeix 50 lectures; la que hi posa la promesa imprimeix 1. A la primera, les 50 crides entren, troben la variable a null —cap no ha acabat encara— i totes disparen la seva lectura abans que la primera assigni res. A la segona, l'assignació de promesaCataleg passa de manera síncrona, abans del primer await, de manera que la crida número 2 ja troba una promesa en curs i es limita a esperar-la. La lliçó general: en una memòria cau asíncrona es guarda l'operació en curs, no el seu resultat.

Conclusió

Escena Viva ja llegeix del disc. src/cataleg-dades.js ha deixat de ser un array incrustat per convertir-se en una capa de dades asíncrona que llegeix dades/esdeveniments.json, l'analitza amb JSON.parse, construeix instàncies d'Esdeveniment amb desDeJSON i tradueix les fallades del sistema de fitxers al vocabulari del domini amb error.codi. La signatura que vam dissenyar al Mòdul 2 va aguantar el canvi sense trencar ningú: només va caldre afegir un await i una frontera amb .catch(). Pel camí has fixat els criteris que governen tot ús de fs d'ara endavant: node:fs/promises per defecte, la versió síncrona únicament a l'arrencada o en scripts d'un sol ús —i has mesurat amb els teus propis ulls els 31 ms de bucle congelat que costa ignorar-ho—, 'utf8' per a text i la seva absència per a binari, error.code en comptes de missatges, intentar i capturar en comptes de preguntar i actuar, l'opció flag per controlar com s'obre el fitxer, i l'escriptura atòmica amb temporal més rename perquè un tall de llum no destrueixi el catàleg. I saps que en una memòria cau asíncrona el que es guarda és la promesa, no el valor.

Però només hem fet servir dues funcions de fs, i el mòdul en té desenes. A la lliçó següent, El Mòdul fs a Fons, sortirem del fitxer únic: metadades amb stat i l'objecte Stats, recorregut de directoris amb readdir i withFileTypes, creació i esborrat d'arbres sencers amb mkdir i rm recursius, l'API FileHandle amb els seus descriptors que cal tancar sense falta, permisos i vigilància de canvis. I ho aplicarem a una cosa que Escena Viva ja necessita: un magatzem d'informes que organitzi les sortides per mes, les llisti i purgui les antigues.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats