Artículo original: Node.js Streams: Everything you need to know

Actualización: Este artículo es ahora parte de mi libro "Node.js Beyond The Basics".

Lee la versión actualizada de este contenido y más sobre Node en jscomplete.com/node-beyond-basics.

Los flujos de Node.js (streams)tienen la reputación de ser difíciles de trabajar, y aún más difíciles de entender. Bueno, tengo buenas noticias para ti - ese ya no es el caso.

A lo largo de los años, los desarrolladores han creado montones de paquetes con el único propósito de facilitar el trabajo con flujos. Pero en este artículo, voy a centrarme en la API nativa de flujos de Node.js.

“Los flujos son la mejor y más incomprendida idea de Node.”

— Dominic Tarr

¿Qué son exactamente los flujos?

Los flujos son colecciones de datos, como las matrices o las cadenas de texto. La diferencia es que estos pueden no estar disponibles todos a la vez y no tienen por qué caber en la memoria. Esto hace que sean realmente potentes cuando se trabaja con grandes cantidades de datos, o con datos que vienen de una fuente externa de uno en uno.

Sin embargo, los flujos no sólo sirven para trabajar con grandes cantidades de datos. También nos ofrecen la posibilidad de componer nuestro código. Al igual que podemos componer poderosos comandos de Linux mediante la canalización de otros comandos más pequeños, podemos hacer exactamente lo mismo en Node con los flujos.

1*Fp3dyVZckIUjPFOp58x-zQ
Compatibilidad con comandos Linux
const grep = ... // Un flujo para la salida de grep
const wc = ... // A stream for the wc input

grep.pipe(wc)

Muchos de los módulos incorporados en Node implementan la interfaz de streaming:

1*lhOvZiDrVbzF8_l8QX3ACw

La lista anterior tiene algunos ejemplos de objetos nativos de Node.js que también son flujos legibles y escribibles. Algunos de estos objetos son flujos tanto legibles como escribibles, como los sockets TCP, zlib y flujos crypto.

Observa que los objetos también están estrechamente relacionados. Mientras que una respuesta HTTP es un flujo legible en el cliente, es un flujo escribible en el servidor. Esto se debe a que en el caso HTTP, básicamente leemos de un objeto (http.IncomingMessage) y escribimos en el otro (http.ServerResponse).

Observe también cómo los flujos stdio ( stdin, stdout, stderr) tienen los tipos de flujo inversos cuando se trata de procesos hijo. Esto permite una manera realmente fácil de canalizar hacia y desde estos flujos desde los flujos stdio del proceso principal.

Un ejemplo práctico de flujos

La teoría está muy bien, pero a menudo no convence al 100%. Veamos un ejemplo que demuestra la diferencia que pueden marcar los flujos en el código en lo que respecta al consumo de memoria.

Primero vamos a crear un archivo grande:

const fs = require('fs');
const archivo = fs.createWriteStream('./big.file');

for(let i=0; i<= 1e6; i++) {
  file.write('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n');
}

file.end();

Mira lo que usé para crear ese gran archivo. ¡Un stream escribible!

El módulo fs se puede utilizar para leer y escribir en archivos utilizando una interfaz de flujo. En el ejemplo anterior, estamos escribiendo en ese big.file a través de un flujo escribible de 1 millón de líneas con un bucle.

Ejecutando el script de arriba se genera un archivo de unos ~400 MB.

Aquí hay un simple servidor web Node diseñado para servir exclusivamente el big.file:

const fs = require('fs');
const servidor = require('http').createServer();

servidor.on('request', (peticion, respuesta) => {
  fs.readFile('./big.file', (error, data) => {
    if (error) throw error;
  
    respuesta.end(data);
  });
});

servidor.listen(8000);

Cuando el servidor reciba una petición, servirá el archivo grande usando el método asíncrono, fs.readFile. Pero bueno, no es que estemos bloqueando el bucle de eventos ni nada. Todo va genial, ¿verdad? ¿Verdad?

Bueno, veamos qué pasa cuando ejecutamos el servidor, nos conectamos a él, y monitorizamos la memoria mientras lo hacemos.

Cuando ejecuté el servidor, empezó con una cantidad normal de memoria, 8.7 MB:

1*125_8HQ4KzJkeBcj1LcEiQ

Luego me conecté al servidor. Observe lo que ocurrió con la memoria consumida:

1*SGJw31T5Q9Zfsk24l2yirg

Vaya, el consumo de memoria ha subido a 434,8 MB.

Básicamente ponemos todo el big.file en memoria antes de escribirlo en el objeto de respuesta. Esto es muy ineficiente.

El objeto de respuesta HTTP (res  el objeto de respuesta HTTP) es también un flujo escribible. Esto significa que si tenemos un flujo legible que representa el contenido de big.file, podemos superponerlas y obtener prácticamente el mismo resultado sin consumir ~400 MB de memoria.

El módulo fs de Node puede proporcionarnos un flujo legible para cualquier archivo utilizando el método createReadStream. Podemos enviarlo al objeto de respuesta:

const fs = require('fs');
const servidor = require('http').createServer();

servidor.on('request', (peticion, respuesta) => {
  const src = fs.createReadStream('./big.file');
  src.pipe(respuesta);
});

servidor.listen(8000);

Ahora, cuando te conectas a este servidor, ocurre algo mágico (fíjate en el consumo de memoria):

1*iWNNIMhF9QmD25Vho6-fRQ

¿Qué es lo que ocurre?

Cuando un cliente pide un archivo tan grande, lo transmitimos de uno en uno, lo que significa que no lo almacenamos en memoria. El uso de memoria creció unos 25 MB y eso es todo.

Puedes llevar este ejemplo al límite. Regenera el archivo big.file con cinco millones de líneas en lugar de sólo un millón, lo que llevaría el archivo a más de 2 GB, y eso es realmente más grande que el límite de búfer por defecto en Node.

Si intentas servir ese archivo utilizando fs.readFile, simplemente no puedes, por defecto (puedes cambiar los límites). Pero con fs.createReadStream, no hay ningún problema en transmitir 2 GB de datos al solicitante, y lo mejor de todo es que el uso de memoria del proceso será aproximadamente el mismo.

¿Listo para aprender sobre flujos ahora?

Flujos 101

Hay cuatro tipos fundamentales de flujos en Node.js: Lectura, escritura, dúplex, y transformación de flujos.

  • Un flujo legible es una abstracción de una fuente desde la que se pueden consumir datos. Un ejemplo de ello es el método fs.createReadStream.
  • Un flujo escribible es una abstracción de un destino en el que se pueden escribir datos. Un ejemplo de ello es el método fs.createWriteStream.
  • Un flujo dúplex es tanto legible como escribible. Un ejemplo de ello es un socket TCP.
  • Un flujo de transformación es básicamente un flujo dúplex que puede utilizarse para modificar o transformar los datos a medida que se escriben y se leen. Un ejemplo de ello es el flujo zlib.createGzip para comprimir los datos usando "gzip". Puedes pensar en un flujo de transformación como una función en la que la entrada es la parte del flujo que se puede escribir y la salida es la parte del flujo que se puede leer. También puedes oír referirse a los flujos de transformación como "flujos pasantes".

Todos los flujos son instancias de EventEmitter. Emiten eventos que pueden utilizarse para leer y escribir datos. Sin embargo, podemos consumir los datos en los flujos de una forma más sencilla utilizando el método pipe.

El método "pipe"

Esta es la línea mágica que debes recordar:

fuenteLegible.pipe(destinoEscribible)

En esta simple línea, estamos canalizando la salida de un flujo legible - la fuente de datos, como la entrada de un flujo escribible - el destino. La fuente tiene que ser un flujo legible y el destino uno escribible. Por supuesto, ambos pueden ser también flujos dúplex/transformados. De hecho, si estamos canalizando a un flujo dúplex, podemos encadenar llamadas de canalización como hacemos en Linux:

fuenteLegible
  .pipe(transformarFlujo1)
  .pipe(transformarFlujo2)
  .pipe(destinoEscribibleFinal)

El método pipe  retorna el flujo de destino, lo que nos permitió hacer el encadenamiento anterior. Para flujos a (legible), b and c (duplex), and d (escribible), podemos:

a.pipe(b).pipe(c).pipe(d)

# Que es equivalente a:
a.pipe(b)
b.pipe(c)
c.pipe(d)

# Que, en Linux, es equivalente a:
$ a | b | c | d

El método pipe es la forma más sencilla de consumir flujos. Por lo general, se recomienda utilizar el método pipe o consumir flujos con eventos, pero evite mezclar estos dos. Usualmente cuando estás usando el método pipe no es necesario utilizar eventos, pero si necesita consumir los flujos de forma más personalizada, los eventos serían el camino a seguir.

Eventos de flujo

Además de leer de una fuente legible y escribir en un destino escribible, el método pipe gestiona automáticamente algunas cosas por el camino. Por ejemplo, gestiona los errores, el fin de los archivos y los casos en los que un flujo es más lento o más rápido que el otro.

Sin embargo, los flujos también se pueden consumir con eventos directamente. A continuación se muestra el código simplificado equivalente a un evento de lo que el método pipehace principalmente para leer y escribir información:

# legible.pipe(escribible)

legible.on('data', (trozo) => {
  escribible.write(trozo);
});

legible.on('end', () => {
  escribible.end();
});

He aquí una lista de los eventos y funciones importantes que pueden utilizarse con flujos legibles y escribibles:

1*HGXpeiF5-hJrOk_8tT2jFA
Node.js avanzado

Los eventos y las funciones están relacionados de algún modo porque suelen utilizarse juntos.

Los eventos más importantes en un flujo legible son:

  • El evento data, que se emite cada vez que el flujo pasa un trozo de datos al consumidor.
  • El evento end, que se emite cuando no hay más datos que consumir del flujo.

Los eventos más importantes en un flujo escribible son:

  • El evento drain, que es una señal de que el flujo escribible puede recibir más datos.
  • El evento finish, que se emite cuando todos los datos se han vaciado en el sistema subyacente.

Los eventos y las funciones pueden combinarse para hacer un uso personalizado y optimizado de los flujos. Para consumir un flujo legible, podemos utilizar los métodos pipe/unpipe, o los métodos read/unshift/resume. Para consumir un flujo escribible, podemos convertirlo en el destino de pipe/unpipe, o simplemente escribir en él con la función write y llame al método end cuando hayamos terminado.

Modos de pausa y fluyente en flujos legibles

Los flujos legibles tienen dos modos principales que afectan a la forma en que podemos consumirlos:

  • Puede estar en modo pausa.
  • O en modo fluido.

Estos modos se denominan a veces "pull" y "push".

Todos los flujos legibles se inician por defecto en el modo pausado, pero pueden cambiarse fácilmente a fluido y de nuevo a pausado cuando sea necesario. A veces, el cambio se produce automáticamente.

Cuando un flujo legible está en modo de pausa, podemos utilizar la función read()para leer del flujo bajo demanda, sin embargo, para un flujo legible en el modo de flujo, los datos fluyen continuamente y tenemos que escuchar los eventos para consumirlos.

En el modo de flujo, los datos pueden perderse si no hay consumidores disponibles para manejarlos. Esta es la razón por la que, cuando tenemos un flujo legible en modo de flujo, necesitamos un manejador de eventos para la data. De hecho, solo añadiendo un manejador de eventos para la data cambia un flujo en pausa al modo de flujo y eliminar el manejador vuelve a poner el flujo en modo pausa. Parte de esto se hace por compatibilidad con la antigua interfaz de flujos de Node.

Para cambiar manualmente entre estos dos modos de flujo, puede utilizar la función resume() y pause().

1*HI-mtispQ13qm8ib5yey3g
Node.js avanzado

Al consumir flujos legibles mediante la función pipe, no tenemos que preocuparnos de estos modos ya que pipe los gestiona automáticamente.

Implementando flujos

Cuando hablamos de streams en Node.js, hay dos tareas principales diferentes:

  • La tarea de implantar los flujos.
  • La tarea de consumirlos.

Hasta ahora sólo hemos hablado de consumir flujos. ¡Pongamos algunos en práctica!

Los ejecutores de los flujos suelen ser los que requiere el módulo stream.

Implementando un flujo escribible

Para implementar un flujo con capacidad de escritura, necesitamos utilizar la función Writable del módulo de flujo.

const { Escribible } = require('flujo');

Podemos implementar un flujo escribible de muchas maneras. Podemos, por ejemplo, extender el constructor Writable si queremos:

class miFlujoEscribible extends Escribible {
}

Sin embargo, prefiero el enfoque más simple del constructor. Simplemente creamos un objeto a partir del constructor Writable y pasarle una serie de opciones. La única opción obligatoria es la función write que expone el trozo de datos que se va a escribir.

const { Escribible } = require('flujo');

const outStream = new Escribible({
  write(trozo, codificacion, callback) {
    console.log(trozo.toString());
    callback();
  }
});

process.stdin.pipe(outStream);

This write method takes three arguments.

  • El trozo suele ser un buffer a no ser que configuremos el flujo de otra manera.
  • El argumento de codificación es necesario en ese caso, pero normalmente podemos ignorarlo.
  • El callback es una función que necesitamos llamar después de que hayamos terminado de procesar el trozo de datos. Es lo que señala si la escritura fue exitosa o no. Para señalar un fallo, llame a la devolución de llamada con un objeto de error.

En outStream, nosotros simplemente imprimimos con la función console.log el trozo como una cadena y llamar a la devolución de llamada después de que sin un error para indicar el éxito. Este es un flujo de eco muy simple y probablemente no tan útil. Se hará eco de todo lo que reciba.

Para consumir este flujo, podemos utilizarlo simplemente con process.stdin, que es un flujo legible, por lo que podemos simplemente canalizar process.stdin hacia nuestro outStream.

Cuando ejecutamos el código anterior, cualquier cosa que escribamos en process.stdin se devolverá utilizando el método outStream console.log.

No es un flujo muy útil de implementar porque en realidad ya está implementado e incorporado. Esto es muy equivalente a process.stdout. Podemos canalizar stdin hacia stdout y obtendremos exactamente la misma función de eco con esta única línea:

process.stdin.pipe(process.stdout);

Implementa un flujo legible

Para implementar un flujo legible, necesitamos la interfaz Readable, y construir un objeto a partir de él, e implementar un método read() en el parámetro de configuración del flujo:

const { Legible } = require('flujo');

const enFlujo = new Legible({
  read() {}
});

Hay una forma sencilla de implementar flujos legibles. Podemos utilizar directamente push para insertar los datos que queremos que consuman los consumidores.

const { Legible } = require('flujo'); 

const enFlujo = new Legible({
  read() {}
});

enFlujo.push('ABCDEFGHIJKLM');
enFlujo.push('NOPQRSTUVWXYZ');

enFlujo.push(null); // No más datos

enFlujo.pipe(process.stdout);

Cuando usamos push en un objeto null, que significa que queremos señalar que el flujo no tiene más datos.

Para consumir este simple flujo de lectura, podemos simplemente canalizarlo en el flujo de escritura process.stdout.

Cuando ejecutemos el código anterior, estaremos leyendo todos los datos de enFlujo y enviarlo a la salida estándar. Muy sencillo, pero también poco eficiente.

Básicamente estamos empujando todos los datos en el flujo antes de canalizarlos a process.stdout. La mejor manera de hacerlo es enviar los datos a petición del consumidor. Podemos hacerlo implementando la función read()en el objeto de configuración:

const enFlujo = new Legible({
  read(tamaño) {
    // hay una demanda de datos... alguien desea leerlos.
  }
});

Cuando se llama al método de lectura en un flujo legible, la implementación puede enviar datos parciales a la cola. Por ejemplo, podemos empujar una letra a la vez, comenzando con el código de carácter 65 (que representa A), y el aumento que en cada empuje:

const enFlujo = new Legible({
  read(tamaño) {
    this.push(String.fromCharCode(this.currentCharCode++));
    if (this.currentCharCode > 90) {
      this.push(null);
    }
  }
});

enFlujo.currentCharCode = 65;

enFlujo.pipe(process.stdout);

Mientras el consumidor está leyendo un flujo legible, el método read continuará disparándose, y empujaremos más letras. Tenemos que detener este ciclo en algún lugar, y es por eso que una sentencia if para empujar null cuando el currentCharCode (código de carácter actual) es mayor que 90 (que representa Z).

Este código es equivalente al más simple con el que empezamos, pero ahora estamos empujando los datos bajo demanda cuando el consumidor lo pide. Usted siempre debe hacer eso.

Implementando flujos Dúplex/Transformación

Con los flujos dúplex, podemos implementar tanto flujos de lectura como de escritura con el mismo objeto. Es como si heredáramos de ambas interfaces.

A continuación se muestra un ejemplo de flujo dúplex que combina los dos ejemplos de escritura y lectura implementados anteriormente:

const { Duplex } = require('flujo');

const inoutStream = new Duplex({
  write(trozo, codificacion, callback) {
    console.log(trozo.toString());
    callback();
  },

  read(tamaño) {
    this.push(String.fromCharCode(this.currentCharCode++));
    if (this.currentCharCode > 90) {
      this.push(null);
    }
  }
});

inoutStream.currentCharCode = 65;

process.stdin.pipe(inoutStream).pipe(process.stdout);

Combinando los métodos, podemos utilizar este flujo dúplex para leer las letras de la A a la Z y también podemos utilizarlo por su función de eco. Canalizamos el flujo legible stdin en este flujo dúplex para utilizar la función de eco y canalizamos el propio flujo dúplex en el archivo escribible stdout para ver las letras de la A a la Z.

Es importante entender que los lados legible y grabable de un flujo dúplex funcionan de forma completamente independiente el uno del otro. Se trata simplemente de una agrupación de dos características en un objeto.

Un flujo de transformación es el flujo dúplex más interesante porque su salida se calcula a partir de su entrada.

Para un flujo de transformación, no tenemos que implementar la función read o write, sólo necesitamos implementar un método transform, que combina ambos. Tiene la firma del método write y podemos utilizarlo para insertar con push los datos también.

Aquí tienes un simple flujo de transformación que devuelve cualquier cosa que escribas en él después de transformarla a formato de mayúsculas:

const { Transform } = require('flujo');

const aMayuscula = new Transform({
  transform(trozo, codificacion, callback) {
    this.push(trozo.toString().toUpperCase());
    callback();
  }
});

process.stdin.pipe(aMayuscula).pipe(process.stdout);

En este flujo de transformación, que estamos consumiendo exactamente igual que en el ejemplo anterior de flujo dúplex, sólo implementamos el método transform(). En ese método, convertimos el trozo en su versión en mayúsculas y, a continuación, se usa el método push en esa versión como la parte legible.

Modo de flujos como objeto

Por defecto, los flujos esperan valores Buffer/String. Hay un marcador objectMode que podemos configurar para que el flujo acepte cualquier objeto JavaScript.

He aquí un ejemplo sencillo para demostrarlo. La siguiente combinación de flujos de transformación permite mapear una cadena de valores separados por comas en un objeto JavaScript. Así “a,b,c,d” se cionvierte a {a: b, c: d}.

const { Transform } = require('flujo');

const separadorDeComa = new Transform({
  modoDeLecturaObjeto: true,
  
  transform(trozo, codificacion, callback) {
    this.push(trozo.toString().trim().split(','));
    callback();
  }
});

const arrayAObjeto = new Transform({
  modoDeLecturaObjeto: true,
  modoDeEscrituraObjeto: true,
  
  transform(trozo, codificacion, callback) {
    const objeto = {};
    for(let i=0; i < trozo.length; i+=2) {
      objeto[trozo[i]] = trozo[i+1];
    }
    this.push(objeto);
    callback();
  }
});

const objetoAString = new Transform({
  modoDeEscrituraObjeto: true,
  
  transform(trozo, codificacion, callback) {
    this.push(JSON.stringify(trozo) + '\n');
    callback();
  }
});

process.stdin
  .pipe(separadorDeComa)
  .pipe(arrayAObjeto)
  .pipe(objetoAString)
  .pipe(process.stdout)

Pasamos la cadena de entrada (por ejemplo, “a,b,c,d”) a través de separadorDeComa que empuja un array como sus datos legibles ([“a”, “b”, “c”, “d”]). Añadir el marcador modoDeEscrituraObjeto en ese flujo es necesario porque estamos empujando un objeto allí, no una cadena.

A continuación, tomamos la matriz y la introducimos en el archivo arrayAObjeto. Necesitamos un marcador modoDeEscrituraObjetopara hacer que ese flujo acepte un objeto. También empujará un objeto (el array de entrada mapeado en un objeto) y por eso también necesitábamos el marcador modoDeLecturaObjeto ahí también. El último flujo objetoAString acepta un objeto pero expulsa una cadena, y por eso sólo necesitábamos un botón modoDeEscrituraObjeto ahí. La parte legible es una cadena normal (el objeto que se convirtió a string).

1*u2kQzUD0ruPpt-xx0UOHoA
Uso del ejemplo anterior

Los flujos de transformación incorporados en Node.js

Node tiene algunos flujos de transformación incorporados muy útiles. A saber, los flujos "zlib" y "crypto".

He aquí un ejemplo que utiliza la función zlib.createGzip() combinados con los flujos fs que se pueden leer y escribir para crear un código para comprimir archivos:

const fs = require('fs');
const zlib = require('zlib');
const archivo = process.argv[2];

fs.crearFlujoDeLectura(archivo)
  .pipe(zlib.crearGzip())
  .pipe(fs.crearFlujoDeEscritura(archivo + '.gz'));

Puedes usar este script para comprimir cualquier archivo que pases como argumento. Estamos canalizando un flujo legible para ese archivo en el flujo de transformación incorporado en "zlib" y luego en un flujo de escritura para el nuevo archivo comprimido. Simple.

Lo bueno de usar tuberías es que podemos combinarlas con eventos si lo necesitamos. Digamos, por ejemplo, que quiero que el usuario vea un indicador de progreso mientras el script está trabajando y un mensaje de "Hecho" cuando el script haya terminado. Ya que el método pipe devuelve el flujo de destino, podemos encadenar también el registro de manejadores de eventos:

const fs = require('fs');
const zlib = require('zlib');
const archivo = process.argv[2];

fs.crearFlujoDeLectura(archivo)
  .pipe(zlib.crearGzip())
  .on('data', () => process.stdout.write('.'))
  .pipe(fs.crearFlujoDeEscritura(archivo + '.zz'))
  .on('finish', () => console.log('Hecho'));

Así que con el método pipe, conseguimos consumir flujos fácilmente, pero aún podemos personalizar más nuestra interacción con esos flujos utilizando eventos cuando sea necesario.

Lo que es genial del método pipe es que podemos utilizarlo para componer nuestro programa pieza a pieza, de una forma mucho más legible. Por ejemplo, en lugar de escuchar el evento data más arriba, podemos simplemente crear un flujo de transformación para informar del progreso, y reemplazar el método .on()  con otra invocación del método .pipe():

const fs = require('fs');
const zlib = require('zlib');
const archivo = process.argv[2];

const { Transform } = require('flujo');

const reportarProgreso = new Transform({
  transform(trozo, encoding, callback) {
    process.stdout.write('.');
    callback(null, trozo);
  }
});

fs.crearFlujoDeLectura(archivo)
  .pipe(zlib.crearGzip())
  .pipe(reportarProgreso)
  .pipe(fs.crearFlujoDeLectura(archivo + '.zz'))
  .on('finish', () => console.log('Hecho'));

El flujo reportProgress es un simple flujo de paso, pero también informa del progreso a la salida estándar. Fíjate en cómo he utilizado el segundo argumento en el método callback() para empujar los datos dentro del método transform(). Esto equivale a empujar primero los datos.

Las aplicaciones de combinar flujos son infinitas. Por ejemplo, si necesitamos encriptar el archivo antes o después de gziparlo, todo lo que tenemos que hacer es canalizar otro flujo de transformación en ese orden exacto que necesitábamos. Podemos utilizar el módulo crypto de Node para eso:

const crypto = require('crypto');
// ...

fs.crearFluejoDeLectura(archivo)
  .pipe(zlib.crearGzip())
  .pipe(crypto.crearCipher('aes192', 'a_secret'))
  .pipe(reportarProgreso)
  .pipe(fs.crearFlujoDeEscritura(archivo + '.zz'))
  .on('finish', () => console.log('Hecho'));

El script anterior comprime y luego encripta el archivo pasado y sólo aquellos que tienen el secreto pueden utilizar el archivo de salida. No podemos descomprimir este archivo con las utilidades normales de descompresión porque está cifrado.

Para poder descomprimir cualquier cosa comprimida con el script anterior, necesitamos utilizar los flujos opuestos para "crypto" y "zlib" en orden inverso, lo cual es sencillo:

fs.crearFlujoDeLectura(archivo)
  .pipe(crypto.crearDecifrador('aes192', 'a_secret'))
  .pipe(zlib.crearGunzip())
  .pipe(reportarProgreso)
  .pipe(fs.crearFlujoDeLectura(archivo.slice(0, -3)))
  .on('finish', () => console.log('Hecho'));

Asumiendo que el archivo pasado es la versión comprimida, el código anterior creará un flujo de lectura a partir de él, lo canalizará en el flujo de "crypto" createDecipher() (utilizando el mismo secreto), canalice la salida de éste archivo en el flujo "fzlib" createGunzip(), y luego escribir las cosas de nuevo a un archivo sin la parte de la extensión.

Eso es todo lo que tengo para este tema. Gracias por leernos. Hasta la próxima.