Most of Node is built on two ideas: objects that emit named events, and streams that move data in chunks instead of loading everything into memory.
EventEmitter
import { EventEmitter, once } from "node:events";
class Order extends EventEmitter {
place(item) {
this.emit("placed", { item, at: "12:00" });
if (item === "unknown") this.emit("error", new Error("no such item"));
}
}
const order = new Order();
order.on("placed", ({ item }) => console.log("email: placed", item));
order.once("placed", () => console.log("analytics: first order only"));
order.on("error", (err) => console.log("handled error:", err.message));
order.place("book");
order.place("unknown");
console.log(order.listenerCount("placed"), order.eventNames());
setTimeout(() => order.emit("shipped", "TRACK-1"), 5);
const [tracking] = await once(order, "shipped"); // promise-based wait
console.log("shipped with", tracking);email: placed book
analytics: first order only
email: placed unknown
handled error: no such item
1 [ 'placed', 'error' ]
shipped with TRACK-1Streams
| Type | Examples |
|---|---|
| Readable | fs.createReadStream, HTTP request (server side), process.stdin |
| Writable | fs.createWriteStream, HTTP response, process.stdout |
| Duplex | TCP socket |
| Transform | zlib.createGzip(), encryption, parsers |
import { Readable, Transform, Writable } from "node:stream";
import { pipeline } from "node:stream/promises";
const source = Readable.from(["alpha\n", "beta\n", "gamma\n"]);
const upper = new Transform({
transform(chunk, _enc, callback) {
callback(null, chunk.toString().toUpperCase());
},
});
const collected = [];
const sink = new Writable({
write(chunk, _enc, callback) {
collected.push(chunk.toString().trim());
callback();
},
});
await pipeline(source, upper, sink); // handles back-pressure and errors
console.log(collected);[ 'ALPHA', 'BETA', 'GAMMA' ]Why use streams instead of reading a whole file?