├── .editorconfig
├── .gitignore
├── 02
└── hello.js
├── 03-fortune
├── 01-fortune-single.js
├── 02-fortune-single-multiline.js
├── 03-fortune-multi.js
├── 04-fortune-multi-json.js
├── 05-read-file.js
├── 06-read-dir.js
├── 07-fortune-cb.js
├── 08-fortune-exit-code.js
├── 09-fortune-console-error.js
├── 10-fortune-txt-files-only.js
├── 11-fortune-argv.js
├── 12-fortune-promises.js
├── 13-fortune-async-await.mjs
├── data
│ ├── 001.txt
│ ├── 002.txt
│ ├── 003.txt
│ ├── 004.txt
│ ├── 005.txt
│ └── 006.txt
└── quotes.json
├── 04-library
├── 01-min.js
├── 02-min-clear.js
├── 03-json.js
├── 04-json-clear.js
├── 05-accept.js
├── 06-accept-types.js
├── 07-accept-complete.js
├── 08-accept-refactor.js
├── 09-routing.js
├── 10-head.js
├── 11-query-params.js
├── 12-html.js
├── 13-keep-alive.js
├── 14-events.js
└── utils.js
├── 05-chat
├── 01-tcp-connection.js
├── 02-tcp-echo.js
├── 03-tcp-buffer.js
├── 04-tcp-buffer-match.js
├── 05-tcp-echo-binary.js
├── 06-tcp-data-count.js
├── 07-chat-broadcasting.js
├── 08-chat-address-port.js
├── 09-chat-broadcast-fixed.js
├── 10-chat-refactor.js
├── 11-chat-nicks.js
├── 12-chat-colors.js
├── 13-chat-pvt.js
├── 14-chat-list.js
├── 15-chat-presence.js
├── 16-events.js
├── 17-clock.js
├── 18-clock-sync.js
├── 19-clock-async.js
├── 20-clock-broken.js
├── 21-clock-broken-async.js
├── 22-chat-lurkers.js
├── chat-utils.js
└── lurkers-detector.js
├── 06-www
├── 01-w3-server.mjs
├── 02-w3-media-types.mjs
├── 03-w3-head.mjs
├── 04-w3-stream.mjs
├── 05-stream-tcp-logger.mjs
├── 06-stream-flowing-paused.mjs
├── 07-w3-writable-logger.mjs
├── 08-w3-transform-gzip.mjs
├── 09-w3-pipe-errors.mjs
├── 10-w3-log-timing.mjs
├── 11-w3-env-vars.mjs
├── 12-w3-index.mjs
├── files
│ ├── favicon.ico
│ ├── index.html
│ ├── kitchen-sink.html
│ ├── licenses
│ │ ├── agpl-3.0.txt
│ │ └── gpl-3.0.txt
│ ├── selfie.jpg
│ └── style.css
├── logger-unsafe.mjs
├── logger.mjs
├── media-types.mjs
└── w3-utils.mjs
├── 07-eloop
├── 01-linear-prime.mjs
├── 02-callback-prime.mjs
├── 03-callback-timeout.mjs
├── 04-timers-poll-check.mjs
├── 05-draining-queues.mjs
├── 06-scheduling-timers.mjs
├── 07-microtasks.mjs
├── 08-next-tick.mjs
├── 09-partitioned.mjs
├── 10-http-prime-blocking.mjs
├── 11-http-prime-partitioned.mjs
├── 12-child-exec-file.mjs
├── 13-child-exec.mjs
├── 14-http-prime-child-node.mjs
├── 15-http-prime-child-c.mjs
├── 16-child-spawn.mjs
├── 17-child-spawn-pipe.mjs
├── 18-http-prime-child-spawn.mjs
├── 19-child-fork.mjs
├── 20-http-prime-child-fork.mjs
├── 21-self-fork.mjs
├── 22-http-prime-cluster.mjs
├── 23-worker-thread.mjs
├── 24-http-prime-workers.mjs
├── 25-http-prime-workers-pool.mjs
├── 26-http-prime-workers-pool-2.mjs
├── cpu-intensive.mjs
├── eloop-utils.mjs
├── isprime-child.mjs
├── isprime-worker.mjs
├── isprime.c
├── isprime.mjs
└── number.txt
├── 08-pkg
├── 01-check-strings.js
├── 02-manipulate-strings.js
├── 03-debugging-time.js
├── 04-open-rad-map.js
├── 05-missing-mod.js
├── 06-shared-counter.js
├── 07-lines-counter.mjs
├── 08-numbered-echo.mjs
├── 09-open-links.mjs
├── 10-feed-reader
│ ├── .eslintrc
│ ├── .gitignore
│ ├── app.mjs
│ ├── get-feeds.mjs
│ ├── package-lock.json
│ ├── package.json
│ └── wrong.mjs
├── links.txt
└── modules
│ ├── bookmark-utils.mjs
│ ├── debug
│ └── index.js
│ ├── is-string.js
│ ├── open
│ ├── open.js
│ └── package.json
│ ├── print-lines-numbers.mjs
│ ├── read-file-lines.mjs
│ ├── read-number.mjs
│ ├── shared-counter
│ ├── counter.js
│ └── package.json
│ └── string-utils
│ └── index.js
├── 09-express
├── 01-apod
│ ├── apod-api.mjs
│ ├── app_v1.mjs
│ ├── app_v2.mjs
│ ├── app_v3.mjs
│ ├── app_v4.mjs
│ ├── app_v5.mjs
│ ├── app_v6.mjs
│ ├── app_v7.mjs
│ ├── app_v8.mjs
│ ├── app_v9.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── mvp.css
│ │ └── saturn-voyager.jpg
│ ├── views
│ │ ├── about.hbs
│ │ └── home.hbs
│ └── views2
│ │ ├── 404.hbs
│ │ ├── 500.hbs
│ │ ├── about.hbs
│ │ ├── apod-full.hbs
│ │ ├── apod-not-found.hbs
│ │ ├── apod-pic.hbs
│ │ ├── apod.hbs
│ │ ├── home.hbs
│ │ └── layout.hbs
├── 02-feed-api
│ ├── app_v1.mjs
│ ├── app_v2.mjs
│ ├── app_v3.mjs
│ ├── app_v4.mjs
│ ├── app_v5.mjs
│ ├── app_v6.mjs
│ ├── app_v7.mjs
│ ├── app_v8.mjs
│ ├── get-feeds.mjs
│ ├── models
│ │ ├── FeedEntry.mjs
│ │ ├── Source.mjs
│ │ └── Source_v1.mjs
│ ├── package-lock.json
│ └── package.json
└── 03-feed-api-router
│ ├── app.mjs
│ ├── get-feeds.mjs
│ ├── models
│ ├── FeedEntry.mjs
│ └── Source.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── routes
│ ├── feeds-update.mjs
│ ├── feeds.mjs
│ └── sources.mjs
│ └── utils.mjs
├── 10-fastify
├── 01-essential
│ ├── 01-hello-fastify.mjs
│ ├── 02-fastify-logging.mjs
│ ├── 03-short-vs-full.mjs
│ ├── 04-params.mjs
│ ├── 05-request.mjs
│ ├── 06-reply-send.mjs
│ ├── 07-reply.mjs
│ ├── 08-errors.mjs
│ ├── package-lock.json
│ └── package.json
├── 02-feed-api-fast
│ ├── get-feeds.mjs
│ ├── models
│ │ ├── FeedEntry.mjs
│ │ └── Source.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── routes
│ │ ├── feeds-update.mjs
│ │ ├── feeds.mjs
│ │ └── sources.mjs
│ └── server.mjs
└── 03-feed-api-improved
│ ├── get-feeds.mjs
│ ├── models
│ ├── FeedEntry.mjs
│ └── Source.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── plugins
│ └── async-utils.mjs
│ ├── routes
│ ├── feeds-update.mjs
│ ├── feeds.mjs
│ └── sources.mjs
│ └── server.mjs
└── README.md
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 4
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/02/hello.js:
--------------------------------------------------------------------------------
1 | function hello(name) {
2 | console.log(`Hello ${name}`);
3 | }
4 | hello("Node.js");
5 |
--------------------------------------------------------------------------------
/03-fortune/01-fortune-single.js:
--------------------------------------------------------------------------------
1 | const quote =
2 | "Any app that can be written in JavaScript, will eventually be written in JavaScript. Jeff Atwood";
3 |
4 | console.log(quote);
5 |
--------------------------------------------------------------------------------
/03-fortune/02-fortune-single-multiline.js:
--------------------------------------------------------------------------------
1 | const quote = `
2 | Any app that can be written in JavaScript,
3 | will eventually be written in JavaScript.
4 | -- Jeff Atwood.
5 | `;
6 |
7 | console.log(quote);
8 |
--------------------------------------------------------------------------------
/03-fortune/03-fortune-multi.js:
--------------------------------------------------------------------------------
1 | const quotes = [
2 | `Any app that can be written in JavaScript,
3 | will eventually be written in JavaScript.
4 | -- Jeff Atwood.
5 | `,
6 | `JavaScript is the only language that I'm aware of
7 | that people feel they don't need to learn before
8 | they start using it.
9 | -- Douglas Crockford
10 | `,
11 | `Code never lies, comments sometimes do.
12 | -- Anonymous
13 | `,
14 | ];
15 |
16 | const randomIdx = Math.floor(Math.random() * quotes.length);
17 |
18 | console.log(quotes[randomIdx]);
19 |
--------------------------------------------------------------------------------
/03-fortune/04-fortune-multi-json.js:
--------------------------------------------------------------------------------
1 | const quotes = require("./quotes.json");
2 |
3 | const randomIdx = Math.floor(Math.random() * quotes.length);
4 |
5 | console.log(quotes[randomIdx]);
6 |
--------------------------------------------------------------------------------
/03-fortune/05-read-file.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | fs.readFile("./data/003.txt", "utf-8", (err, data) => {
4 | if (err) {
5 | console.log("Error while reading quote file");
6 | return;
7 | }
8 | console.log(data);
9 | });
10 |
--------------------------------------------------------------------------------
/03-fortune/06-read-dir.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | fs.readdir("./data", (err, files) => {
4 | if (err) {
5 | console.log("Error while reading data directory");
6 | return;
7 | }
8 | console.log(files);
9 | });
10 |
--------------------------------------------------------------------------------
/03-fortune/07-fortune-cb.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | const QUOTES_DIR = "./data";
4 |
5 | fs.readdir(QUOTES_DIR, (err, files) => {
6 | if (err) {
7 | console.log("Error while reading data directory");
8 | return;
9 | }
10 |
11 | const randomIdx = Math.floor(Math.random() * files.length);
12 | const quoteFile = `${QUOTES_DIR}/${files[randomIdx]}`;
13 |
14 | fs.readFile(quoteFile, "utf-8", (err, data) => {
15 | if (err) {
16 | console.log("Error while reading quote file");
17 | return;
18 | }
19 | console.log(data.toString());
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/03-fortune/08-fortune-exit-code.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | const QUOTES_DIR = "./data";
4 |
5 | fs.readdir(QUOTES_DIR, (err, files) => {
6 | if (err) {
7 | console.log("Error while reading data directory");
8 | process.exitCode = 1;
9 | return;
10 | }
11 |
12 | const randomIdx = Math.floor(Math.random() * files.length);
13 | const quoteFile = `${QUOTES_DIR}/${files[randomIdx]}`;
14 |
15 | fs.readFile(quoteFile, "utf-8", (err, data) => {
16 | if (err) {
17 | console.log("Error while reading quote file");
18 | process.exitCode = 1;
19 | return;
20 | }
21 | console.log(data);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/03-fortune/09-fortune-console-error.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | const QUOTES_DIR = "./data";
4 |
5 | fs.readdir(QUOTES_DIR, (err, files) => {
6 | if (err) {
7 | console.error(`Error while reading ${QUOTES_DIR} directory`);
8 | process.exitCode = 1;
9 | return;
10 | }
11 |
12 | const randomIdx = Math.floor(Math.random() * files.length);
13 | const quoteFile = `${QUOTES_DIR}/${files[randomIdx]}`;
14 |
15 | fs.readFile(quoteFile, "utf-8", (err, data) => {
16 | if (err) {
17 | console.error(`Error while reading ${quoteFile} file`);
18 | process.exitCode = 1;
19 | return;
20 | }
21 | console.log(data.toString());
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/03-fortune/10-fortune-txt-files-only.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | const QUOTES_DIR = "./data";
4 |
5 | fs.readdir(QUOTES_DIR, { withFileTypes: true }, (err, files) => {
6 | if (err) {
7 | console.error(`Error while reading ${QUOTES_DIR} directory`);
8 | process.exitCode = 1;
9 | return;
10 | }
11 |
12 | const txtFiles = files
13 | .filter((f) => f.isFile() && f.name.endsWith(".txt"))
14 | .map((f) => f.name);
15 |
16 | const randomIdx = Math.floor(Math.random() * txtFiles.length);
17 | const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`;
18 |
19 | fs.readFile(quoteFile, "utf-8", (err, data) => {
20 | if (err) {
21 | console.error(`Error while reading ${quoteFile} file`);
22 | process.exitCode = 1;
23 | return;
24 | }
25 | console.log(data.toString());
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/03-fortune/11-fortune-argv.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | const QUOTES_DIR = process.argv[2];
4 |
5 | fs.readdir(QUOTES_DIR, { withFileTypes: true }, (err, files) => {
6 | if (err) {
7 | console.error(`Error while reading ${QUOTES_DIR} directory`);
8 | process.exitCode = 1;
9 | return;
10 | }
11 |
12 | const txtFiles = files
13 | .filter((f) => f.isFile() && f.name.endsWith(".txt"))
14 | .map((f) => f.name);
15 |
16 | const randomIdx = Math.floor(Math.random() * txtFiles.length);
17 | const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`;
18 |
19 | fs.readFile(quoteFile, "utf-8", (err, data) => {
20 | if (err) {
21 | console.error(`Error while reading ${quoteFile} file`);
22 | process.exitCode = 1;
23 | return;
24 | }
25 | console.log(data.toString());
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/03-fortune/12-fortune-promises.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs/promises");
2 |
3 | const QUOTES_DIR = process.argv[2];
4 |
5 | fs.readdir(QUOTES_DIR, { withFileTypes: true })
6 | .then((files) => {
7 | const txtFiles = files
8 | .filter((f) => f.isFile() && f.name.endsWith(".txt"))
9 | .map((f) => f.name);
10 |
11 | const randomIdx = Math.floor(Math.random() * txtFiles.length);
12 | const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`;
13 |
14 | return fs.readFile(quoteFile, "utf-8");
15 | })
16 | .then((data) => {
17 | console.log(data.toString());
18 | })
19 | .catch((err) => {
20 | console.error(`Error: ${err.message}`);
21 | process.exitCode = 1;
22 | return;
23 | });
24 |
--------------------------------------------------------------------------------
/03-fortune/13-fortune-async-await.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 |
3 | const QUOTES_DIR = process.argv[2];
4 |
5 | try {
6 | const files = await fs.readdir(QUOTES_DIR, { withFileTypes: true });
7 | const txtFiles = files
8 | .filter((f) => f.isFile() && f.name.endsWith(".txt"))
9 | .map((f) => f.name);
10 |
11 | const randomIdx = Math.floor(Math.random() * txtFiles.length);
12 | const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`;
13 |
14 | const data = await fs.readFile(quoteFile, "utf-8");
15 | console.log(data.toString());
16 | } catch (err) {
17 | console.error(`Error: ${err.message}`);
18 | process.exitCode = 1;
19 | }
20 |
--------------------------------------------------------------------------------
/03-fortune/data/001.txt:
--------------------------------------------------------------------------------
1 | Non si abita un Paese, si abita una lingua.
2 | Una patria è questo, e niente altro.
3 | -- Emil Cioran
--------------------------------------------------------------------------------
/03-fortune/data/002.txt:
--------------------------------------------------------------------------------
1 | Pubblicare un libro comporta lo stesso genere di noie
2 | di un matrimonio o di un funerale.
3 | -- Emil Cioran
--------------------------------------------------------------------------------
/03-fortune/data/003.txt:
--------------------------------------------------------------------------------
1 | Un libro che lascia il lettore uguale
2 | a com'era prima di leggerlo è un libro fallito.
3 | -- Emil Cioran
--------------------------------------------------------------------------------
/03-fortune/data/004.txt:
--------------------------------------------------------------------------------
1 | Si tira un aforisma come si tira uno schiaffo.
2 | -- Emil Cioran
--------------------------------------------------------------------------------
/03-fortune/data/005.txt:
--------------------------------------------------------------------------------
1 | Non si scrive perché si ha qualcosa da dire
2 | ma perché si ha voglia di dire qualcosa.
3 | -- Emil Cioran
4 |
--------------------------------------------------------------------------------
/03-fortune/data/006.txt:
--------------------------------------------------------------------------------
1 | Il fatto che la vita non abbia alcun senso è una ragione di vivere
2 | – la sola, del resto.
3 | -- Emil Cioran
--------------------------------------------------------------------------------
/03-fortune/quotes.json:
--------------------------------------------------------------------------------
1 | [
2 | "Any app that can be written in JavaScript,\nwill eventually be written in JavaScript.\n\t\t\t-- Jeff Atwood",
3 | "JavaScript is the only language that I'm aware of\nthat people feel they don't need to learn before\nthey start using it.\n\t\t\t-- Douglas Crockford",
4 | "Code never lies, comments sometimes do.\n\t\t\t-- Anonymous"
5 | ]
6 |
--------------------------------------------------------------------------------
/04-library/01-min.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const server = http.createServer((req, res) => {
4 | res.end("Benvenuto nella biblioteca HTTP");
5 | });
6 |
7 | server.listen(3000, () => {
8 | console.log("Server running");
9 | });
10 |
--------------------------------------------------------------------------------
/04-library/02-min-clear.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | const server = http.createServer((req, res) => {
7 | res.statusCode = 200;
8 | res.end("Benvenuto nella biblioteca HTTP");
9 | });
10 |
11 | server.listen(port, host, () => {
12 | console.log(`Server running at http://${host}:${port}/`);
13 | });
14 |
--------------------------------------------------------------------------------
/04-library/03-json.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | const server = http.createServer((req, res) => {
7 | res.statusCode = 200;
8 | res.end('{"message": "Benvenuto nella biblioteca HTTP"}');
9 | });
10 |
11 | server.listen(port, host, () => {
12 | console.log(`Server running at http://${host}:${port}/`);
13 | });
14 |
--------------------------------------------------------------------------------
/04-library/04-json-clear.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | const server = http.createServer((req, res) => {
7 | res.statusCode = 200;
8 | res.setHeader("Content-Type", "application/json");
9 | res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" }));
10 | });
11 |
12 | server.listen(port, host, () => {
13 | console.log(`Server running at http://${host}:${port}/`);
14 | });
15 |
--------------------------------------------------------------------------------
/04-library/05-accept.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | const server = http.createServer((req, res) => {
7 | const acceptJson = req.headers.accept === "application/json";
8 | const acceptText = req.headers.accept === "text/plain";
9 |
10 | if (acceptJson) {
11 | res.statusCode = 200;
12 | res.setHeader("Content-Type", "application/json");
13 | res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" }));
14 | } else if (acceptText) {
15 | res.statusCode = 200;
16 | res.setHeader("Content-Type", "text/plain");
17 | res.end("Benvenuto nella biblioteca HTTP");
18 | } else {
19 | res.statusCode = 406;
20 | res.end();
21 | }
22 | });
23 |
24 | server.listen(port, host, () => {
25 | console.log(`Server running at http://${host}:${port}/`);
26 | });
27 |
--------------------------------------------------------------------------------
/04-library/06-accept-types.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | function resJson(res) {
7 | res.statusCode = 200;
8 | res.setHeader("Content-Type", "application/json");
9 | res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" }));
10 | }
11 |
12 | function resText(res) {
13 | res.statusCode = 200;
14 | res.setHeader("Content-Type", "text/plain");
15 | res.end("Benvenuto nella biblioteca HTTP");
16 | }
17 |
18 | const server = http.createServer((req, res) => {
19 | const acceptJson = req.headers.accept === "application/json";
20 | const acceptText = req.headers.accept === "text/plain";
21 | const acceptAnyText = req.headers.accept === "text/*";
22 | const acceptAnyType = req.headers.accept === "*/*";
23 |
24 | if (acceptJson) {
25 | resJson(res);
26 | } else if (acceptText || acceptAnyText || acceptAnyType) {
27 | resText(res);
28 | } else {
29 | res.statusCode = 406;
30 | res.end();
31 | }
32 | });
33 |
34 | server.listen(port, host, () => {
35 | console.log(`Server running at http://${host}:${port}/`);
36 | });
37 |
--------------------------------------------------------------------------------
/04-library/07-accept-complete.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | function resJson(res) {
7 | res.statusCode = 200;
8 | res.setHeader("Content-Type", "application/json");
9 | res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" }));
10 | }
11 | function resText(res) {
12 | res.statusCode = 200;
13 | res.setHeader("Content-Type", "text/plain");
14 | res.end("Benvenuto nella biblioteca HTTP");
15 | }
16 | const server = http.createServer((req, res) => {
17 | // ["text/html", "application/xml;q=0.9", ...]
18 | const acceptList = req.headers.accept.split(",");
19 | // ["text/html", "application/xml", ...]
20 | const acceptedTypes = acceptList.map((a) => a.split(";")[0]);
21 |
22 | const acceptJson = acceptedTypes.includes("application/json");
23 | const acceptText = acceptedTypes.includes("text/plain");
24 | const acceptAnyText = acceptedTypes.includes("text/*");
25 | const acceptAnyType = acceptedTypes.includes("*/*");
26 |
27 | if (acceptJson) {
28 | resJson(res);
29 | } else if (acceptText || acceptAnyText || acceptAnyType) {
30 | resText(res);
31 | } else {
32 | res.statusCode = 406;
33 | res.end();
34 | }
35 | });
36 |
37 | server.listen(port, host, () => {
38 | console.log(`Server running at http://${host}:${port}/`);
39 | });
40 |
--------------------------------------------------------------------------------
/04-library/08-accept-refactor.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const { getAcceptedTypes, resText, resJson } = require("./utils");
3 |
4 | const host = "127.0.0.1";
5 | const port = 3000;
6 |
7 | const data = {
8 | json: JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" }),
9 | text: "Benvenuto nella biblioteca HTTP",
10 | };
11 | const server = http.createServer((req, res) => {
12 | const accepts = getAcceptedTypes(req);
13 | if (accepts.json) {
14 | resJson(res, data.json);
15 | } else if (accepts.textPlain || accepts.text || accepts.any) {
16 | resText(res, data.text);
17 | } else {
18 | res.statusCode = 406;
19 | res.end();
20 | }
21 | });
22 |
23 | server.listen(port, host, () => {
24 | console.log(`Server running at http://${host}:${port}/`);
25 | });
26 |
--------------------------------------------------------------------------------
/04-library/09-routing.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const { getAcceptedTypes, resJson, resText } = require("./utils");
3 |
4 | const host = "127.0.0.1";
5 | const port = 3000;
6 |
7 | const library = {
8 | message: "Benvenuto nella biblioteca HTTP",
9 | books: [
10 | { author: "Naomi Klein", title: "Shock economy" },
11 | { author: "Serge Latouche", title: "L'invenzione dell'economia" },
12 | {
13 | author: "Yanis Varoufakis",
14 | title: "È l'economia che cambia il mondo",
15 | },
16 | ],
17 | };
18 |
19 | const routes = {
20 | "/": {
21 | getText: () => library.message,
22 | getJson: function () {
23 | return JSON.stringify({ message: library.message });
24 | },
25 | },
26 | "/books": {
27 | getText: () =>
28 | library.books.reduce((acc, cur) => {
29 | acc += `${cur.author} - ${cur.title}\n`;
30 | return acc;
31 | }, ""),
32 | getJson: function () {
33 | return JSON.stringify(library.books);
34 | },
35 | },
36 | };
37 |
38 | const server = http.createServer((req, res) => {
39 | const route = routes[req.url];
40 | if (!route) {
41 | res.statusCode = 404;
42 | res.end();
43 | return;
44 | }
45 |
46 | const accepts = getAcceptedTypes(req);
47 | if (accepts.json) {
48 | resJson(res, route.getJson());
49 | } else if (accepts.textPlain || accepts.text || accepts.any) {
50 | resText(res, route.getText());
51 | } else {
52 | res.statusCode = 406;
53 | res.end();
54 | }
55 | });
56 |
57 | server.listen(port, host, () => {
58 | console.log(`Server running at http://${host}:${port}/`);
59 | });
60 |
--------------------------------------------------------------------------------
/04-library/10-head.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const { getAcceptedTypes, resJson, resText } = require("./utils");
3 |
4 | const host = "127.0.0.1";
5 | const port = 3000;
6 |
7 | const library = {
8 | message: "Benvenuto nella biblioteca HTTP",
9 | books: [
10 | { author: "Naomi Klein", title: "Shock economy" },
11 | { author: "Serge Latouche", title: "L'invenzione dell'economia" },
12 | {
13 | author: "Yanis Varoufakis",
14 | title: "È l'economia che cambia il mondo",
15 | },
16 | ],
17 | };
18 |
19 | const routes = {
20 | "/": {
21 | getText: () => library.message,
22 | getJson: () => {
23 | return JSON.stringify({ message: library.message });
24 | },
25 | },
26 | "/books": {
27 | getText: () =>
28 | library.books.reduce((acc, cur) => {
29 | acc += `${cur.author} - ${cur.title}\n`;
30 | return acc;
31 | }, ""),
32 | getJson: () => {
33 | return JSON.stringify(library.books);
34 | },
35 | },
36 | };
37 |
38 | const server = http.createServer((req, res) => {
39 | const route = routes[req.url];
40 | if (!route) {
41 | res.statusCode = 404;
42 | res.end();
43 | return;
44 | }
45 |
46 | if (req.method === "HEAD") {
47 | res.statusCode = 204;
48 | res.end();
49 | } else if (req.method === "GET") {
50 | const accepts = getAcceptedTypes(req);
51 | if (accepts.json) {
52 | resJson(res, route.getJson());
53 | } else if (accepts.textPlain || accepts.text || accepts.any) {
54 | resText(res, route.getText());
55 | } else {
56 | res.statusCode = 406;
57 | res.end();
58 | }
59 | } else {
60 | res.statusCode = 405;
61 | res.end();
62 | }
63 | });
64 |
65 | server.listen(port, host, () => {
66 | console.log(`Server running at http://${host}:${port}/`);
67 | });
68 |
--------------------------------------------------------------------------------
/04-library/11-query-params.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const { getAcceptedTypes, resJson, resText } = require("./utils");
3 |
4 | const host = "127.0.0.1";
5 | const port = 3000;
6 |
7 | const library = {
8 | message: "Benvenuto nella biblioteca HTTP",
9 | books: [
10 | { author: "Felix Martin", title: "Denaro" },
11 | {
12 | author: "Yanis Varoufakis",
13 | title: "È l'economia che cambia il mondo",
14 | },
15 | { author: "Yanis Varoufakis", title: "Adulti nella stanza" },
16 | { author: "Naomi Klein", title: "Shock economy" },
17 | { author: "Serge Latouche", title: "L'invenzione dell'economia" },
18 | { author: "Thomas Piketty", title: "Il capitale nel XXI secolo" },
19 | ],
20 | };
21 |
22 | function findBooks(q) {
23 | if (q) {
24 | return library.books.filter(
25 | (b) => b.author.includes(q) || b.title.includes(q)
26 | );
27 | } else {
28 | return library.books;
29 | }
30 | }
31 |
32 | const routes = {
33 | "/": {
34 | getText: () => library.message,
35 | getJson: () => {
36 | return JSON.stringify({ message: library.message });
37 | },
38 | },
39 | "/books": {
40 | getText: (q) => {
41 | return findBooks(q).reduce((acc, cur) => {
42 | acc += `${cur.author} - ${cur.title}\n`;
43 | return acc;
44 | }, "");
45 | },
46 | getJson: (q) => {
47 | return JSON.stringify(findBooks(q));
48 | },
49 | },
50 | };
51 |
52 | const server = http.createServer((req, res) => {
53 | const { pathname, searchParams } = new URL(
54 | req.url,
55 | `http://${req.headers.host}`
56 | );
57 |
58 | const route = routes[pathname];
59 | if (!route) {
60 | res.statusCode = 404;
61 | res.end();
62 | return;
63 | }
64 |
65 | if (req.method === "HEAD") {
66 | res.statusCode = 204;
67 | res.end();
68 | } else if (req.method === "GET") {
69 | const accepts = getAcceptedTypes(req);
70 | const q = searchParams.get("q");
71 | if (accepts.json) {
72 | resJson(res, route.getJson(q));
73 | } else if (accepts.textPlain || accepts.text || accepts.any) {
74 | resText(res, route.getText(q));
75 | } else {
76 | res.statusCode = 406;
77 | res.end();
78 | }
79 | } else {
80 | res.statusCode = 405;
81 | res.end();
82 | }
83 | });
84 |
85 | server.listen(port, host, () => {
86 | console.log(`Server running at http://${host}:${port}/`);
87 | });
88 |
--------------------------------------------------------------------------------
/04-library/12-html.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const { getAcceptedTypes, resJson, resText, resHtml } = require("./utils");
3 |
4 | const host = "127.0.0.1";
5 | const port = 3000;
6 |
7 | const library = {
8 | message: "Benvenuto nella biblioteca HTTP",
9 | books: [
10 | { author: "Felix Martin", title: "Denaro" },
11 | {
12 | author: "Yanis Varoufakis",
13 | title: "È l'economia che cambia il mondo",
14 | },
15 | { author: "Yanis Varoufakis", title: "Adulti nella stanza" },
16 | { author: "Naomi Klein", title: "Shock economy" },
17 | { author: "Serge Latouche", title: "L'invenzione dell'economia" },
18 | { author: "Thomas Piketty", title: "Il capitale nel XXI secolo" },
19 | ],
20 | };
21 |
22 | function findBooks(q) {
23 | if (q) {
24 | return library.books.filter(
25 | (b) => b.author.includes(q) || b.title.includes(q)
26 | );
27 | } else {
28 | return library.books;
29 | }
30 | }
31 |
32 | function htmlLayout(title, body) {
33 | return `
34 |
35 |
36 |
37 |
38 | ${title}
39 |
40 |
41 | ${body}
42 |
43 | `;
44 | }
45 |
46 | const routes = {
47 | "/": {
48 | getText: () => library.message,
49 | getJson: () => {
50 | return JSON.stringify({ message: library.message });
51 | },
52 | getHtml: function () {
53 | return htmlLayout(
54 | library.message,
55 | `${library.message} Libri `
56 | );
57 | },
58 | },
59 | "/books": {
60 | getText: (q) => {
61 | return findBooks(q).reduce((acc, cur) => {
62 | acc += `${cur.author} - ${cur.title}\n`;
63 | return acc;
64 | }, "");
65 | },
66 | getJson: (q) => {
67 | return JSON.stringify(findBooks(q));
68 | },
69 | getHtml: function (q) {
70 | const booksLi = findBooks(q)
71 | .map((b) => `${b.author} - ${b.title} `)
72 | .join(" ");
73 | return htmlLayout(
74 | "Libri",
75 | `
76 | Home `
77 | );
78 | },
79 | },
80 | };
81 |
82 | const server = http.createServer((req, res) => {
83 | const { pathname, searchParams } = new URL(
84 | req.url,
85 | `http://${req.headers.host}`
86 | );
87 |
88 | const route = routes[pathname];
89 | if (!route) {
90 | res.statusCode = 404;
91 | res.end();
92 | return;
93 | }
94 |
95 | if (req.method === "HEAD") {
96 | res.statusCode = 204;
97 | res.end();
98 | } else if (req.method === "GET") {
99 | const accepts = getAcceptedTypes(req);
100 | const q = searchParams.get("q");
101 |
102 | if (accepts.textHtml || accepts.any) {
103 | resHtml(res, route.getHtml(q));
104 | } else if (accepts.json) {
105 | resJson(res, route.getJson(q));
106 | } else if (accepts.textPlain || accepts.text) {
107 | resText(res, route.getText(q));
108 | } else {
109 | res.statusCode = 406;
110 | res.end();
111 | }
112 | } else {
113 | res.statusCode = 405;
114 | res.end();
115 | }
116 | });
117 |
118 | server.listen(port, host, () => {
119 | console.log(`Server running at http://${host}:${port}/`);
120 | });
121 |
--------------------------------------------------------------------------------
/04-library/13-keep-alive.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | const server = http.createServer({ keepAliveTimeout: 50000 }, (req, res) => {
7 | res.end("Keeping the connection alive!");
8 | });
9 |
10 | server.listen(port, host, () => {
11 | console.log(`Server running at http://${host}:${port}/`);
12 | });
13 |
--------------------------------------------------------------------------------
/04-library/14-events.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 |
3 | const host = "127.0.0.1";
4 | const port = 3000;
5 |
6 | const server = http.createServer({ keepAliveTimeout: 50000 });
7 | server.on("connection", () => console.log("connection"));
8 | server.on("request", (req, res) => {
9 | console.log("request");
10 | res.end("Hello from the server");
11 | });
12 |
13 | server.listen(port, host, () => {
14 | console.log(`Server running at http://${host}:${port}/`);
15 | });
16 |
--------------------------------------------------------------------------------
/04-library/utils.js:
--------------------------------------------------------------------------------
1 | function resJson(res, data) {
2 | res.statusCode = 200;
3 | res.setHeader("Content-Type", "application/json");
4 | res.end(data);
5 | }
6 | function resText(res, data) {
7 | res.statusCode = 200;
8 | res.setHeader("Content-Type", "text/plain");
9 | res.end(data);
10 | res.end();
11 | }
12 | function resHtml(res, data) {
13 | res.statusCode = 200;
14 | res.setHeader("Content-Type", "text/html");
15 | res.end(data);
16 | }
17 | function getAcceptedTypes(req) {
18 | const acceptHeaderVal = req.headers.accept || "*/*";
19 | // ["text/html", "application/xml;q=0.9", ...]
20 | const acceptList = acceptHeaderVal.split(",");
21 | // ["text/html", "application/xml", ...]
22 | const acceptedTypes = acceptList.map((a) => a.split(";")[0]);
23 |
24 | const json = acceptedTypes.includes("application/json");
25 | const textPlain = acceptedTypes.includes("text/plain");
26 | const textHtml = acceptedTypes.includes("text/html");
27 | const text = acceptedTypes.includes("text/*");
28 | const any = acceptedTypes.includes("*/*");
29 | return { json, textPlain, textHtml, text, any };
30 | }
31 | module.exports = { resJson, resText, resHtml, getAcceptedTypes };
32 |
--------------------------------------------------------------------------------
/05-chat/01-tcp-connection.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const port = 5050;
3 | const host = "127.0.0.1";
4 |
5 | const server = net.createServer();
6 |
7 | server.listen(port, host, () => {
8 | console.log(`TCP server running at ${host} on port ${port}`);
9 | });
10 |
11 | server.on("connection", () => console.log("connection"));
12 |
--------------------------------------------------------------------------------
/05-chat/02-tcp-echo.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const port = 5050;
3 | const host = "127.0.0.1";
4 |
5 | const server = net.createServer();
6 |
7 | server.listen(port, host, () => {
8 | console.log(`TCP server running at ${host} on port ${port}`);
9 | });
10 |
11 | server.on("connection", function (sock) {
12 | sock.on("data", function (data) {
13 | sock.write("ECHO: " + data);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/05-chat/03-tcp-buffer.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const port = 5050;
3 | const host = "127.0.0.1";
4 | const { inspect } = require("util");
5 |
6 | const server = net.createServer();
7 |
8 | server.listen(port, host, () => {
9 | console.log(`TCP server running at ${host} on port ${port}`);
10 | });
11 |
12 | server.on("connection", function (sock) {
13 | sock.on("data", function (data) {
14 | sock.write(`Received ${inspect(data)}\n`);
15 | sock.write("ECHO: " + data);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/05-chat/04-tcp-buffer-match.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const { inspect } = require("util");
3 | const { Buffer } = require("buffer");
4 |
5 | const port = 5050;
6 | const host = "127.0.0.1";
7 |
8 | const server = net.createServer();
9 |
10 | server.listen(port, host, () => {
11 | console.log(`TCP server running at ${host} on port ${port}`);
12 | });
13 |
14 | server.on("connection", function (sock) {
15 | sock.on("data", function (data) {
16 | sock.write(`Received ${inspect(data)}\n`);
17 | if (data.equals(Buffer.from("ping\x0d\x0a", "utf-8"))) {
18 | sock.write("data matches ping\\x0d\\x0a\n");
19 | }
20 | if (data.equals(Buffer.from("ping\r\n", "utf-8"))) {
21 | sock.write("data matches ping\\r\\n\n");
22 | }
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/05-chat/05-tcp-echo-binary.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 |
3 | const port = 5050;
4 | const host = "127.0.0.1";
5 |
6 | const server = net.createServer();
7 |
8 | server.listen(port, host, () => {
9 | console.log(`TCP server running at ${host} on port ${port}`);
10 | });
11 |
12 | server.on("connection", function (sock) {
13 | sock.on("data", function (data) {
14 | sock.write(data);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/05-chat/06-tcp-data-count.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 |
3 | const port = 5050;
4 | const host = "127.0.0.1";
5 |
6 | const server = net.createServer();
7 |
8 | server.listen(port, host, () => {
9 | console.log(`TCP server running at ${host} on port ${port}`);
10 | });
11 |
12 | let dataCount = 0;
13 | server.on("connection", function (sock) {
14 | sock.on("data", function (data) {
15 | dataCount++;
16 | console.log(
17 | `dataCount is ${dataCount} and data contains ${data.byteLength} bytes`
18 | );
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/05-chat/07-chat-broadcasting.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const port = 5050;
3 | const host = "127.0.0.1";
4 |
5 | const server = net.createServer();
6 | server.listen(port, host, () => {
7 | console.log("TCP Server is running on port " + port + ".");
8 | });
9 |
10 | let sockets = [];
11 | server.on("connection", function (sock) {
12 | sockets.push(sock);
13 | sock.on("data", function (data) {
14 | sockets.forEach((sock) => sock.write(data));
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/05-chat/08-chat-address-port.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const port = 5050;
3 | const host = "127.0.0.1";
4 |
5 | const server = net.createServer();
6 | server.listen(port, host, () => {
7 | console.log("TCP Server is running on port " + port + ".");
8 | });
9 |
10 | let sockets = [];
11 | server.on("connection", function (sock) {
12 | console.log(`CONNECTED: ${sock.remoteAddress}:${sock.remotePort}`);
13 | sockets.push(sock);
14 | sock.on("data", function (data) {
15 | const sender = `${sock.remoteAddress}:${sock.remotePort}`;
16 | sockets.forEach((s) => s.write(`<${sender}> ${data}`));
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/05-chat/09-chat-broadcast-fixed.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const { isSameSocket } = require("./chat-utils");
3 | const port = 5050;
4 | const host = "127.0.0.1";
5 |
6 | const server = net.createServer();
7 | server.listen(port, host, () => {
8 | console.log("TCP Server is running on port " + port + ".");
9 | });
10 |
11 | let sockets = [];
12 | server.on("connection", function (sock) {
13 | console.log(`CONNECTED: ${sock.remoteAddress}:${sock.remotePort}`);
14 | sockets.push(sock);
15 | sock.on("data", function (data) {
16 | const sender = `${sock.remoteAddress}:${sock.remotePort}`;
17 | sockets.forEach((s) => {
18 | if (!isSameSocket(s, sock)) {
19 | s.write(`<${sender}> ${data}`);
20 | }
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/05-chat/10-chat-refactor.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const {
3 | broadcastMessage,
4 | getSocketsExcluding,
5 | removeCRLF,
6 | } = require("./chat-utils");
7 | const port = 5050;
8 | const host = "127.0.0.1";
9 |
10 | const server = net.createServer();
11 | server.listen(port, host, () => {
12 | console.log("TCP Server is running on port " + port + ".");
13 | });
14 |
15 | let sockets = [];
16 |
17 | function processMessage(sock, message) {
18 | const cleanMsg = removeCRLF(message);
19 | const sender = `${sock.remoteAddress}:${sock.remotePort}`;
20 | broadcastMessage(
21 | getSocketsExcluding(sockets, sock),
22 | `<${sender}> ${cleanMsg}\n`
23 | );
24 | }
25 |
26 | server.on("connection", function (sock) {
27 | console.log(`CONNECTED: ${sock.remoteAddress}:${sock.remotePort}`);
28 | sockets.push(sock);
29 | sock.on("data", function (data) {
30 | processMessage(sock, data.toString());
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/05-chat/11-chat-nicks.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const {
3 | broadcastMessage,
4 | getSocketsExcluding,
5 | removeCRLF,
6 | socketToId,
7 | } = require("./chat-utils");
8 | const port = 5050;
9 | const host = "127.0.0.1";
10 |
11 | const server = net.createServer();
12 | server.listen(port, host, () => {
13 | console.log("TCP Server is running on port " + port + ".");
14 | });
15 |
16 | let sockets = [];
17 | let namesMap = {};
18 |
19 | function setName(sock, name) {
20 | namesMap[socketToId(sock)] = name;
21 | }
22 | function getName(sock) {
23 | return namesMap[socketToId(sock)];
24 | }
25 |
26 | function processMessage(sock, message) {
27 | const cleanMsg = removeCRLF(message);
28 |
29 | if (cleanMsg.startsWith("/nick ") /* space intended*/) {
30 | const oldName = getName(sock);
31 | const [_, name] = cleanMsg.split(" ");
32 | setName(sock, name);
33 | broadcastMessage(sockets, `${oldName} is now ${name}\n`);
34 | } else {
35 | broadcastMessage(
36 | getSocketsExcluding(sockets, sock),
37 | `<${getName(sock)}> ${cleanMsg}\n`
38 | );
39 | }
40 | }
41 |
42 | server.on("connection", function (sock) {
43 | console.log(`CONNECTED: ${socketToId(sock)}`);
44 |
45 | sockets.push(sock);
46 | setName(sock, socketToId(sock));
47 |
48 | sock.on("data", function (data) {
49 | processMessage(sock, data.toString());
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/05-chat/12-chat-colors.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const {
3 | broadcastMessage,
4 | getSocketsExcluding,
5 | removeCRLF,
6 | socketToId,
7 | colorGrey,
8 | } = require("./chat-utils");
9 | const port = 5050;
10 | const host = "127.0.0.1";
11 |
12 | const server = net.createServer();
13 | server.listen(port, host, () => {
14 | console.log("TCP Server is running on port " + port + ".");
15 | });
16 |
17 | let sockets = [];
18 | let namesMap = {};
19 |
20 | function setName(sock, name) {
21 | namesMap[socketToId(sock)] = name;
22 | }
23 | function getName(sock) {
24 | return namesMap[socketToId(sock)];
25 | }
26 |
27 | function processMessage(sock, message) {
28 | const cleanMsg = removeCRLF(message);
29 |
30 | if (cleanMsg.startsWith("/nick ") /* space intended*/) {
31 | const oldName = getName(sock);
32 | const [_, name] = cleanMsg.split(" ");
33 | setName(sock, name);
34 | broadcastMessage(
35 | sockets,
36 | `${colorGrey(`${oldName} is now ${name}`)}\n`
37 | );
38 | } else {
39 | broadcastMessage(
40 | getSocketsExcluding(sockets, sock),
41 | `<${getName(sock)}> ${cleanMsg}\n`
42 | );
43 | }
44 | }
45 |
46 | server.on("connection", function (sock) {
47 | console.log(`CONNECTED: ${socketToId(sock)}`);
48 |
49 | sockets.push(sock);
50 | setName(sock, socketToId(sock));
51 |
52 | sock.on("data", function (data) {
53 | processMessage(sock, data.toString());
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/05-chat/13-chat-pvt.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const {
3 | broadcastMessage,
4 | getSocketsExcluding,
5 | removeCRLF,
6 | socketToId,
7 | colorGrey,
8 | colorGreen,
9 | parseNickMessage,
10 | parsePvtMessage,
11 | } = require("./chat-utils");
12 | const port = 5050;
13 | const host = "127.0.0.1";
14 |
15 | const server = net.createServer();
16 | server.listen(port, host, () => {
17 | console.log("TCP Server is running on port " + port + ".");
18 | });
19 |
20 | let sockets = [];
21 | let namesMap = {};
22 |
23 | function setName(sock, name) {
24 | namesMap[socketToId(sock)] = name;
25 | }
26 | function getName(sock) {
27 | return namesMap[socketToId(sock)];
28 | }
29 | function getSocketByName(sockets, name) {
30 | return sockets.find((s) => getName(s) === name);
31 | }
32 |
33 | function processMessage(sock, message) {
34 | const cleanMsg = removeCRLF(message);
35 |
36 | if (cleanMsg.startsWith("/nick ") /* space intended*/) {
37 | const oldName = getName(sock);
38 | const name = parseNickMessage(cleanMsg);
39 | setName(sock, name);
40 | broadcastMessage(
41 | sockets,
42 | `${colorGrey(`${oldName} is now ${name}`)}\n`
43 | );
44 | } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) {
45 | const [receiver, pvtMsg] = parsePvtMessage(cleanMsg);
46 | const receiverSock = getSocketByName(sockets, receiver);
47 | const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`);
48 | receiverSock.write(`${preMsg} ${pvtMsg}\n`);
49 | } else {
50 | broadcastMessage(
51 | getSocketsExcluding(sockets, sock),
52 | `<${getName(sock)}> ${cleanMsg}\n`
53 | );
54 | }
55 | }
56 |
57 | server.on("connection", function (sock) {
58 | console.log(`CONNECTED: ${socketToId(sock)}`);
59 |
60 | sockets.push(sock);
61 | setName(sock, socketToId(sock));
62 |
63 | sock.on("data", function (data) {
64 | processMessage(sock, data.toString());
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/05-chat/14-chat-list.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const {
3 | broadcastMessage,
4 | getSocketsExcluding,
5 | removeCRLF,
6 | socketToId,
7 | colorGrey,
8 | colorGreen,
9 | parseNickMessage,
10 | parsePvtMessage,
11 | } = require("./chat-utils");
12 | const port = 5050;
13 | const host = "127.0.0.1";
14 |
15 | const server = net.createServer();
16 | server.listen(port, host, () => {
17 | console.log("TCP Server is running on port " + port + ".");
18 | });
19 |
20 | let sockets = [];
21 | let namesMap = {};
22 |
23 | function setName(sock, name) {
24 | namesMap[socketToId(sock)] = name;
25 | }
26 | function getName(sock) {
27 | return namesMap[socketToId(sock)];
28 | }
29 | function getSocketByName(sockets, name) {
30 | return sockets.find((s) => getName(s) === name);
31 | }
32 |
33 | function processMessage(sock, message) {
34 | const cleanMsg = removeCRLF(message);
35 |
36 | if (cleanMsg.startsWith("/nick ") /* space intended*/) {
37 | const oldName = getName(sock);
38 | const name = parseNickMessage(cleanMsg);
39 | setName(sock, name);
40 | broadcastMessage(
41 | sockets,
42 | `${colorGrey(`${oldName} is now ${name}`)}\n`
43 | );
44 | } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) {
45 | const [receiver, pvtMsg] = parsePvtMessage(cleanMsg);
46 | const receiverSock = getSocketByName(sockets, receiver);
47 | const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`);
48 | receiverSock.write(`${preMsg} ${pvtMsg}\n`);
49 | } else if (cleanMsg === "/list") {
50 | const preMsg = colorGrey(`(only visible to you)`);
51 | const usersString = sockets.map(getName).join(",");
52 | sock.write(`${preMsg} Users are: ${usersString}\n`);
53 | } else {
54 | broadcastMessage(
55 | getSocketsExcluding(sockets, sock),
56 | `<${getName(sock)}> ${cleanMsg}\n`
57 | );
58 | }
59 | }
60 |
61 | server.on("connection", function (sock) {
62 | console.log(`CONNECTED: ${socketToId(sock)}`);
63 |
64 | sockets.push(sock);
65 | setName(sock, socketToId(sock));
66 |
67 | sock.on("data", function (data) {
68 | processMessage(sock, data.toString());
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/05-chat/15-chat-presence.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const {
3 | broadcastMessage,
4 | getSocketsExcluding,
5 | removeCRLF,
6 | socketToId,
7 | colorGrey,
8 | colorGreen,
9 | parseNickMessage,
10 | parsePvtMessage,
11 | } = require("./chat-utils");
12 | const port = 5050;
13 | const host = "127.0.0.1";
14 |
15 | const server = net.createServer();
16 | server.listen(port, host, () => {
17 | console.log("TCP Server is running on port " + port + ".");
18 | });
19 |
20 | let sockets = [];
21 | let namesMap = {};
22 |
23 | function setName(sock, name) {
24 | namesMap[socketToId(sock)] = name;
25 | }
26 | function getName(sock) {
27 | return namesMap[socketToId(sock)];
28 | }
29 | function getSocketByName(sockets, name) {
30 | return sockets.find((s) => getName(s) === name);
31 | }
32 |
33 | function processMessage(sock, message) {
34 | const cleanMsg = removeCRLF(message);
35 |
36 | if (cleanMsg.startsWith("/nick ") /* space intended*/) {
37 | const oldName = getName(sock);
38 | const name = parseNickMessage(cleanMsg);
39 | setName(sock, name);
40 | broadcastMessage(
41 | sockets,
42 | `${colorGrey(`${oldName} is now ${name}`)}\n`
43 | );
44 | } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) {
45 | const [receiver, pvtMsg] = parsePvtMessage(cleanMsg);
46 | const receiverSock = getSocketByName(sockets, receiver);
47 | const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`);
48 | receiverSock.write(`${preMsg} ${pvtMsg}\n`);
49 | } else if (cleanMsg === "/list") {
50 | const preMsg = colorGrey(`(only visible to you)`);
51 | const usersString = sockets.map(getName).join(",");
52 | sock.write(`${preMsg} Users are: ${usersString}\n`);
53 | } else {
54 | broadcastMessage(
55 | getSocketsExcluding(sockets, sock),
56 | `<${getName(sock)}> ${cleanMsg}\n`
57 | );
58 | }
59 | }
60 | const joinedMessage = (sock) =>
61 | `${colorGrey(`${getName(sock)} joined the chat`)}\n`;
62 | const leftMessage = (sock) =>
63 | `${colorGrey(`${getName(sock)} left the chat`)}\n`;
64 |
65 | server.on("connection", function (sock) {
66 | console.log(`CONNECTED: ${socketToId(sock)}`);
67 |
68 | sockets.push(sock);
69 | setName(sock, socketToId(sock));
70 | broadcastMessage(sockets, joinedMessage(sock));
71 |
72 | sock.on("data", function (data) {
73 | processMessage(sock, data.toString());
74 | });
75 |
76 | sock.on("close", function () {
77 | sockets = getSocketsExcluding(sockets, sock);
78 | broadcastMessage(sockets, leftMessage(sock));
79 | console.log("CLOSED: " + socketToId(sock));
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/05-chat/16-events.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require("events");
2 |
3 | const emitter = new EventEmitter();
4 | emitter.on("event", () => {
5 | console.log("an event occurred!");
6 | });
7 |
8 | emitter.emit("event");
9 |
--------------------------------------------------------------------------------
/05-chat/17-clock.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require("events");
2 |
3 | function getTimeString() {
4 | const now = new Date();
5 | const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`;
6 | return time;
7 | }
8 |
9 | class Clock extends EventEmitter {}
10 |
11 | const clock = new Clock();
12 |
13 | clock.on("tick", () => {
14 | console.log(`The clock ticked: ${getTimeString()}`);
15 | });
16 |
17 | setInterval(() => {
18 | clock.emit("tick");
19 | }, 1000);
20 |
--------------------------------------------------------------------------------
/05-chat/18-clock-sync.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require("events");
2 |
3 | function getTimeString() {
4 | const now = new Date();
5 | const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`;
6 | return time;
7 | }
8 |
9 | class Clock extends EventEmitter {}
10 |
11 | const clock = new Clock();
12 |
13 | clock.on("tick", () => {
14 | console.log(`[1] The clock ticked ${getTimeString()}`);
15 | });
16 | clock.on("tick", () => {
17 | console.log(`[2] The clock ticked ${getTimeString()}`);
18 | });
19 | clock.on("tick", () => {
20 | console.log(`[3] The clock ticked ${getTimeString()}`);
21 | });
22 |
23 | setInterval(() => {
24 | clock.emit("tick");
25 | console.log(`Post tick at ${getTimeString()}`);
26 | }, 1000);
27 |
--------------------------------------------------------------------------------
/05-chat/19-clock-async.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require("events");
2 |
3 | function getTimeString() {
4 | const now = new Date();
5 | const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`;
6 | return time;
7 | }
8 |
9 | class Clock extends EventEmitter {}
10 |
11 | const clock = new Clock();
12 |
13 | clock.on("tick", () => {
14 | setImmediate(() => {
15 | console.log(`[1] The clock ticked ${getTimeString()}`);
16 | });
17 | });
18 | clock.on("tick", () => {
19 | setImmediate(() => {
20 | console.log(`[2] The clock ticked ${getTimeString()}`);
21 | });
22 | });
23 |
24 | clock.on("tick", () => {
25 | setImmediate(() => {
26 | console.log(`[3] The clock ticked ${getTimeString()}`);
27 | });
28 | });
29 |
30 | setInterval(() => {
31 | clock.emit("tick");
32 | console.log(`Post tick at ${getTimeString()}`);
33 | }, 1000);
34 |
--------------------------------------------------------------------------------
/05-chat/20-clock-broken.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require("events");
2 |
3 | function getTimeString() {
4 | const now = new Date();
5 | const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`;
6 | return time;
7 | }
8 |
9 | class Clock extends EventEmitter {}
10 | const clock = new Clock();
11 |
12 | clock.on("tick", () => {
13 | console.log(`The clock ticked ${getTimeString()}`);
14 | });
15 | clock.on("error", (err) => console.log(`Got ${err}`));
16 |
17 | setInterval(() => {
18 | clock.emit("tick");
19 | console.log(`Post tick at ${getTimeString()}`);
20 | }, 1000);
21 |
22 | setTimeout(() => {
23 | clock.emit("error", new Error("An error"));
24 | }, 3000);
25 |
--------------------------------------------------------------------------------
/05-chat/21-clock-broken-async.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require("events");
2 |
3 | function getTimeString() {
4 | const now = new Date();
5 | const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`;
6 | return time;
7 | }
8 |
9 | class Clock extends EventEmitter {}
10 | const clock = new Clock({ captureRejections: true });
11 |
12 | clock.on("tick", async () => {
13 | throw new Error("Broken listener");
14 | });
15 | clock.on("error", (err) => console.log(`Got ${err}`));
16 |
17 | setInterval(() => {
18 | clock.emit("tick");
19 | console.log(`Post tick at ${getTimeString()}`);
20 | }, 1000);
21 |
--------------------------------------------------------------------------------
/05-chat/22-chat-lurkers.js:
--------------------------------------------------------------------------------
1 | const net = require("net");
2 | const {
3 | broadcastMessage,
4 | getSocketsExcluding,
5 | removeCRLF,
6 | socketToId,
7 | colorGrey,
8 | colorGreen,
9 | parseNickMessage,
10 | parsePvtMessage,
11 | } = require("./chat-utils");
12 | const LurkersDetector = require("./lurkers-detector");
13 | const port = 5050;
14 | const host = "127.0.0.1";
15 |
16 | const server = net.createServer();
17 | server.listen(port, host, () => {
18 | console.log("TCP Server is running on port " + port + ".");
19 | });
20 |
21 | let sockets = [];
22 | let namesMap = {};
23 | let ld = new LurkersDetector(30);
24 | ld.on("lurker detected", (name) => {
25 | console.log(`${name} is a lurker!`);
26 | const sock = getSocketByName(sockets, name);
27 | sock.resetAndDestroy();
28 | });
29 |
30 | function setName(sock, name) {
31 | namesMap[socketToId(sock)] = name;
32 | }
33 | function getName(sock) {
34 | return namesMap[socketToId(sock)];
35 | }
36 | function getSocketByName(sockets, name) {
37 | return sockets.find((s) => getName(s) === name);
38 | }
39 |
40 | function processMessage(sock, message) {
41 | const cleanMsg = removeCRLF(message);
42 |
43 | if (cleanMsg.startsWith("/nick ") /* space intended*/) {
44 | const oldName = getName(sock);
45 | const name = parseNickMessage(cleanMsg);
46 | setName(sock, name);
47 | broadcastMessage(
48 | sockets,
49 | `${colorGrey(`${oldName} is now ${name}`)}\n`
50 | );
51 | ld.renameUser(oldName, name);
52 | } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) {
53 | const [receiver, pvtMsg] = parsePvtMessage(cleanMsg);
54 | const receiverSock = getSocketByName(sockets, receiver);
55 | const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`);
56 | receiverSock.write(`${preMsg} ${pvtMsg}\n`);
57 | } else if (cleanMsg === "/list") {
58 | const preMsg = colorGrey(`(only visible to you)`);
59 | const usersString = sockets.map(getName).join(",");
60 | sock.write(`${preMsg} Users are: ${usersString}\n`);
61 | } else {
62 | broadcastMessage(
63 | getSocketsExcluding(sockets, sock),
64 | `<${getName(sock)}> ${cleanMsg}\n`
65 | );
66 | }
67 | }
68 | const joinedMessage = (sock) =>
69 | `${colorGrey(`${getName(sock)} joined the chat`)}\n`;
70 | const leftMessage = (sock) =>
71 | `${colorGrey(`${getName(sock)} left the chat`)}\n`;
72 |
73 | server.on("connection", function (sock) {
74 | console.log(`CONNECTED: ${socketToId(sock)}`);
75 |
76 | sockets.push(sock);
77 | setName(sock, socketToId(sock));
78 | broadcastMessage(sockets, joinedMessage(sock));
79 | ld.addUser(getName(sock));
80 |
81 | sock.on("data", function (data) {
82 | ld.touchUser(getName(sock));
83 | processMessage(sock, data.toString());
84 | });
85 |
86 | sock.on("close", function () {
87 | sockets = getSocketsExcluding(sockets, sock);
88 | broadcastMessage(sockets, leftMessage(sock));
89 | console.log("CLOSED: " + socketToId(sock));
90 | ld.removeUser(getName(sock));
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/05-chat/chat-utils.js:
--------------------------------------------------------------------------------
1 | function isSameSocket(s1, s2) {
2 | return (
3 | s1.remoteAddress === s2.remoteAddress && s1.remotePort === s2.remotePort
4 | );
5 | }
6 | function broadcastMessage(sockets, message) {
7 | sockets.forEach((sock) => sock.write(message));
8 | }
9 | function getSocketsExcluding(sockets, sock) {
10 | return sockets.filter((s) => !isSameSocket(s, sock));
11 | }
12 | function removeCRLF(str) {
13 | return str.replace(/[\r\n]+$/, "");
14 | }
15 | function socketToId(sock) {
16 | return `${sock.remoteAddress}:${sock.remotePort}`;
17 | }
18 | function colorGrey(str) {
19 | return `\x1b[97;100m${str}\x1b[0m`;
20 | }
21 | function colorGreen(str) {
22 | return `\x1b[97;42m${str}\x1b[0m`;
23 | }
24 | function parsePvtMessage(msg) {
25 | const [_, receiver, ...rest] = msg.split(" ");
26 | const pvtMsg = rest.join(" ");
27 | return [receiver, pvtMsg];
28 | }
29 | function parseNickMessage(msg) {
30 | const [_, name] = msg.split(" ");
31 | return name;
32 | }
33 |
34 | module.exports = {
35 | isSameSocket,
36 | broadcastMessage,
37 | getSocketsExcluding,
38 | removeCRLF,
39 | socketToId,
40 | colorGrey,
41 | colorGreen,
42 | parseNickMessage,
43 | parsePvtMessage,
44 | };
45 |
--------------------------------------------------------------------------------
/05-chat/lurkers-detector.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require("events");
2 |
3 | class LurkersDetector extends EventEmitter {
4 | constructor(tSec) {
5 | super();
6 | this.users = {};
7 | this.timeoutMS = tSec * 1000;
8 | }
9 | addUser(name) {
10 | this.users[name] = setTimeout(() => {
11 | this.emit("lurker detected", name);
12 | }, this.timeoutMS);
13 | }
14 | touchUser(name) {
15 | clearTimeout(this.users[name]);
16 | this.addUser(name);
17 | }
18 | renameUser(oldName, newName) {
19 | this.removeUser(oldName);
20 | this.addUser(newName);
21 | }
22 | removeUser(name) {
23 | clearTimeout(this.users[name]);
24 | delete this.users[name];
25 | }
26 | }
27 |
28 | module.exports = LurkersDetector;
29 |
--------------------------------------------------------------------------------
/06-www/01-w3-server.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 |
4 | const host = "127.0.0.1";
5 | const port = 3000;
6 | const root = "files";
7 |
8 | const server = http.createServer();
9 | server.on("request", async (req, res) => {
10 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
11 | const file = `${root}${pathname}`;
12 | console.log(`Requested ${file} file`);
13 | try {
14 | const data = await fs.readFile(file);
15 | res.end(data);
16 | } catch (e) {
17 | console.error(e);
18 | res.statusCode = 404;
19 | res.end();
20 | }
21 | });
22 |
23 | server.listen(port, host, () => {
24 | console.log(`Web server running at http://${host}:${port}/`);
25 | });
26 |
--------------------------------------------------------------------------------
/06-www/02-w3-media-types.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 |
6 | const host = "127.0.0.1";
7 | const port = 3000;
8 | const root = "files";
9 |
10 | const server = http.createServer();
11 | server.on("request", async (req, res) => {
12 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
13 | const file = `${root}${pathname}`;
14 |
15 | console.log(`Requested ${file} file`);
16 |
17 | const mimeType = mimeMap.get(extname(pathname));
18 | if (mimeType) {
19 | res.setHeader("Content-Type", mimeType);
20 | }
21 |
22 | try {
23 | const data = await fs.readFile(file);
24 | res.end(data);
25 | } catch (e) {
26 | console.error(e);
27 | res.statusCode = 404;
28 | res.end();
29 | }
30 | });
31 |
32 | server.listen(port, host, () => {
33 | console.log(`Web server running at http://${host}:${port}/`);
34 | });
35 |
--------------------------------------------------------------------------------
/06-www/03-w3-head.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 |
6 | const host = "127.0.0.1";
7 | const port = 3000;
8 | const root = "files";
9 |
10 | const server = http.createServer();
11 | server.on("request", async (req, res) => {
12 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
13 | if (!isGET && !isHEAD) {
14 | res.statusCode = 405;
15 | res.end();
16 | return;
17 | }
18 |
19 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
20 | const file = `${root}${pathname}`;
21 | console.log(`Requested ${file} file`);
22 |
23 | let fh;
24 | try {
25 | fh = await fs.open(file);
26 | } catch (e) {
27 | console.error(e);
28 | res.statusCode = 404;
29 | res.end();
30 | return;
31 | }
32 |
33 | const mimeType = mimeMap.get(extname(pathname));
34 | if (mimeType) {
35 | res.setHeader("Content-Type", mimeType);
36 | }
37 |
38 | if (isHEAD) {
39 | const fileStat = await fh.stat();
40 | res.setHeader("Content-Length", fileStat.size);
41 | res.statusCode = 200;
42 | res.end();
43 | await fh.close();
44 | return;
45 | }
46 |
47 | try {
48 | const data = await fh.readFile();
49 | res.end(data);
50 | } catch (e) {
51 | console.error(e);
52 | res.statusCode = 500;
53 | res.end();
54 | } finally {
55 | await fh.close();
56 | }
57 | });
58 |
59 | server.listen(port, host, () => {
60 | console.log(`Web server running at http://${host}:${port}/`);
61 | });
62 |
--------------------------------------------------------------------------------
/06-www/04-w3-stream.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 |
6 | const host = "127.0.0.1";
7 | const port = 3000;
8 | const root = "files";
9 |
10 | const server = http.createServer();
11 | server.on("request", async (req, res) => {
12 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
13 | if (!isGET && !isHEAD) {
14 | res.statusCode = 405;
15 | res.end();
16 | return;
17 | }
18 |
19 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
20 | const file = `${root}${pathname}`;
21 | console.log(`Requested ${file} file`);
22 |
23 | let fh;
24 | try {
25 | fh = await fs.open(file);
26 | } catch (e) {
27 | console.error(e);
28 | res.statusCode = 404;
29 | res.end();
30 | return;
31 | }
32 |
33 | const mimeType = mimeMap.get(extname(pathname));
34 | if (mimeType) {
35 | res.setHeader("Content-Type", mimeType);
36 | }
37 |
38 | if (isHEAD) {
39 | const fileStat = await fh.stat();
40 | res.setHeader("Content-Length", fileStat.size);
41 | res.statusCode = 200;
42 | res.end();
43 | await fh.close();
44 | return;
45 | }
46 |
47 | const fileStream = fh.createReadStream();
48 | fileStream.on("data", (chunk) => res.write(chunk));
49 | fileStream.on("end", () => res.end());
50 | fileStream.on("error", (e) => {
51 | console.error(e);
52 | res.statusCode = 500;
53 | res.end();
54 | });
55 | });
56 |
57 | server.listen(port, host, () => {
58 | console.log(`Web server running at http://${host}:${port}/`);
59 | });
60 |
--------------------------------------------------------------------------------
/06-www/05-stream-tcp-logger.mjs:
--------------------------------------------------------------------------------
1 | import net from "net";
2 |
3 | const server = net.createServer();
4 |
5 | server.listen(5050, "127.0.0.1");
6 |
7 | server.on("connection", function (sock) {
8 | sock.on("data", function (data) {
9 | console.log("Received data event with: " + data);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/06-www/06-stream-flowing-paused.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 |
3 | const fh = await fs.open("06-stream-flowing-paused.mjs");
4 | const fileStream = fh.createReadStream();
5 |
6 | console.log(`Is Readable flowing? ${fileStream.readableFlowing}`);
7 | console.log("Waiting 2 seconds before adding the data event listener...");
8 |
9 | setTimeout(() => {
10 | console.log(`Is Readable flowing? ${fileStream.readableFlowing}`);
11 | fileStream.on("data", (chunk) => console.log(chunk));
12 | console.log(`Is Readable flowing? ${fileStream.readableFlowing}`);
13 | }, 2000);
14 |
--------------------------------------------------------------------------------
/06-www/07-w3-writable-logger.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 | import { log } from "./logger.mjs";
6 |
7 | const host = "127.0.0.1";
8 | const port = 3000;
9 | const root = "files";
10 |
11 | const server = http.createServer();
12 | server.on("request", async (req, res) => {
13 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
14 | if (!isGET && !isHEAD) {
15 | res.statusCode = 405;
16 | res.end();
17 | return;
18 | }
19 |
20 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
21 | const file = `${root}${pathname}`;
22 |
23 | log(`Requested ${file} file`);
24 |
25 | let fh;
26 | try {
27 | fh = await fs.open(file);
28 | } catch (e) {
29 | console.error(e);
30 | res.statusCode = 404;
31 | res.end();
32 | return;
33 | }
34 |
35 | const mimeType = mimeMap.get(extname(pathname));
36 | if (mimeType) {
37 | res.setHeader("Content-Type", mimeType);
38 | }
39 |
40 | if (isHEAD) {
41 | const fileStat = await fh.stat();
42 | res.setHeader("Content-Length", fileStat.size);
43 | res.statusCode = 200;
44 | res.end();
45 | await fh.close();
46 | return;
47 | }
48 |
49 | const fileStream = fh.createReadStream();
50 | fileStream.on("data", (chunk) => res.write(chunk));
51 | fileStream.on("end", () => res.end());
52 | fileStream.on("error", (e) => {
53 | console.error(e);
54 | res.statusCode = 500;
55 | res.end();
56 | });
57 | });
58 |
59 | server.listen(port, host, () => {
60 | console.log(`Web server running at http://${host}:${port}/`);
61 | });
62 |
--------------------------------------------------------------------------------
/06-www/08-w3-transform-gzip.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 | import { log } from "./logger.mjs";
6 | import { createGzip } from "zlib";
7 |
8 | const host = "127.0.0.1";
9 | const port = 3000;
10 | const root = "files";
11 |
12 | const server = http.createServer();
13 | server.on("request", async (req, res) => {
14 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
15 | if (!isGET && !isHEAD) {
16 | res.statusCode = 405;
17 | res.end();
18 | return;
19 | }
20 |
21 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
22 | const file = `${root}${pathname}`;
23 |
24 | log(`Requested ${file} file`);
25 |
26 | let fh;
27 | try {
28 | fh = await fs.open(file);
29 | } catch (e) {
30 | console.error(e);
31 | res.statusCode = 404;
32 | res.end();
33 | return;
34 | }
35 |
36 | const mimeType = mimeMap.get(extname(pathname));
37 | if (mimeType) {
38 | res.setHeader("Content-Type", mimeType);
39 | }
40 |
41 | if (isHEAD) {
42 | const fileStat = await fh.stat();
43 | res.setHeader("Content-Length", fileStat.size);
44 | res.statusCode = 200;
45 | res.end();
46 | await fh.close();
47 | return;
48 | }
49 |
50 | res.setHeader("Content-Encoding", "gzip");
51 | const fileStream = fh.createReadStream();
52 | const gzipTransform = createGzip();
53 | fileStream.pipe(gzipTransform).pipe(res);
54 | fileStream.on("error", (e) => {
55 | console.error(e);
56 | res.statusCode = 500;
57 | res.end();
58 | });
59 | });
60 |
61 | server.listen(port, host, () => {
62 | console.log(`Web server running at http://${host}:${port}/`);
63 | });
64 |
--------------------------------------------------------------------------------
/06-www/09-w3-pipe-errors.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 | import { log } from "./logger.mjs";
6 | import { createGzip } from "zlib";
7 |
8 | const host = "127.0.0.1";
9 | const port = 3000;
10 | const root = "files";
11 |
12 | const server = http.createServer();
13 | server.on("request", async (req, res) => {
14 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
15 | if (!isGET && !isHEAD) {
16 | res.statusCode = 405;
17 | res.end();
18 | return;
19 | }
20 |
21 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
22 | const file = `${root}${pathname}`;
23 |
24 | log(`Requested ${file} file`);
25 |
26 | let fh;
27 | try {
28 | fh = await fs.open(file);
29 | } catch (e) {
30 | console.error(e);
31 | res.statusCode = 404;
32 | res.end();
33 | return;
34 | }
35 |
36 | const mimeType = mimeMap.get(extname(pathname));
37 | if (mimeType) {
38 | res.setHeader("Content-Type", mimeType);
39 | }
40 |
41 | if (isHEAD) {
42 | const fileStat = await fh.stat();
43 | res.setHeader("Content-Length", fileStat.size);
44 | res.statusCode = 200;
45 | res.end();
46 | await fh.close();
47 | return;
48 | }
49 |
50 | res.setHeader("Content-Encoding", "gzip");
51 | const fileStream = fh.createReadStream();
52 | const gzipTransform = createGzip();
53 | const handleStreamError = (err) => {
54 | console.error(err);
55 | fileStream.destroy();
56 | gzipTransform.destroy();
57 | res.statusCode = 500;
58 | };
59 | fileStream
60 | .on("error", handleStreamError)
61 | .pipe(gzipTransform)
62 | .on("error", handleStreamError)
63 | .pipe(res);
64 | });
65 |
66 | server.listen(port, host, () => {
67 | console.log(`Web server running at http://${host}:${port}/`);
68 | });
69 |
--------------------------------------------------------------------------------
/06-www/10-w3-log-timing.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 | import { log } from "./logger.mjs";
6 | import { createGzip } from "zlib";
7 | import { hrtime } from "process";
8 |
9 | const host = "127.0.0.1";
10 | const port = 3000;
11 | const root = "files";
12 |
13 | const server = http.createServer();
14 | server.on("request", async (req, res) => {
15 | const startTime = hrtime.bigint();
16 |
17 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
18 | if (!isGET && !isHEAD) {
19 | res.statusCode = 405;
20 | res.end();
21 | return;
22 | }
23 |
24 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
25 | const file = `${root}${pathname}`;
26 |
27 | res.on("finish", () => {
28 | const endTime = hrtime.bigint();
29 | const duration = (endTime - startTime) / BigInt(1e6);
30 | const timestamp = new Date().toISOString();
31 | const ip = req.socket.remoteAddress;
32 | const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`;
33 | log(`${ip} - ${timestamp} ${httpDetails} - ${duration}ms`);
34 | });
35 |
36 | let fh;
37 | try {
38 | fh = await fs.open(file);
39 | } catch (e) {
40 | console.error(e);
41 | res.statusCode = 404;
42 | res.end();
43 | return;
44 | }
45 |
46 | const mimeType = mimeMap.get(extname(pathname));
47 | if (mimeType) {
48 | res.setHeader("Content-Type", mimeType);
49 | }
50 |
51 | if (isHEAD) {
52 | const fileStat = await fh.stat();
53 | res.setHeader("Content-Length", fileStat.size);
54 | res.statusCode = 200;
55 | res.end();
56 | await fh.close();
57 | return;
58 | }
59 |
60 | res.setHeader("Content-Encoding", "gzip");
61 | const fileStream = fh.createReadStream();
62 | const gzipTransform = createGzip();
63 | const handleStreamError = (err) => {
64 | console.error(err);
65 | fileStream.destroy();
66 | gzipTransform.destroy();
67 | res.statusCode = 500;
68 | };
69 | fileStream
70 | .on("error", handleStreamError)
71 | .pipe(gzipTransform)
72 | .on("error", handleStreamError)
73 | .pipe(res);
74 | });
75 |
76 | server.listen(port, host, () => {
77 | console.log(`Web server running at http://${host}:${port}/`);
78 | });
79 |
--------------------------------------------------------------------------------
/06-www/11-w3-env-vars.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import fs from "fs/promises";
3 | import mimeMap from "./media-types.mjs";
4 | import { extname } from "path";
5 | import { log } from "./logger.mjs";
6 | import { createGzip } from "zlib";
7 | import { hrtime } from "process";
8 | import { exitIfNotDir } from "./w3-utils.mjs";
9 |
10 | const host = process.env.WEB_HOST || "127.0.0.1";
11 | const port = process.env.WEB_PORT || 3000;
12 | const root = process.env.WEB_ROOT || "files";
13 |
14 | await exitIfNotDir(root);
15 |
16 | const server = http.createServer();
17 | server.on("request", async (req, res) => {
18 | const startTime = hrtime.bigint();
19 |
20 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
21 | if (!isGET && !isHEAD) {
22 | res.statusCode = 405;
23 | res.end();
24 | return;
25 | }
26 |
27 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
28 | const file = `${root}${pathname}`;
29 |
30 | res.on("finish", () => {
31 | const endTime = hrtime.bigint();
32 | const duration = (endTime - startTime) / BigInt(1e6);
33 | const timestamp = new Date().toISOString();
34 | const ip = req.socket.remoteAddress;
35 | const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`;
36 | log(`${ip} - ${timestamp} ${httpDetails} - ${duration}ms`);
37 | });
38 |
39 | let fh;
40 | try {
41 | fh = await fs.open(file);
42 | } catch (e) {
43 | console.error(e);
44 | res.statusCode = 404;
45 | res.end();
46 | return;
47 | }
48 |
49 | const mimeType = mimeMap.get(extname(pathname));
50 | if (mimeType) {
51 | res.setHeader("Content-Type", mimeType);
52 | }
53 |
54 | if (isHEAD) {
55 | const fileStat = await fh.stat();
56 | res.setHeader("Content-Length", fileStat.size);
57 | res.statusCode = 200;
58 | res.end();
59 | await fh.close();
60 | return;
61 | }
62 |
63 | res.setHeader("Content-Encoding", "gzip");
64 | const fileStream = fh.createReadStream();
65 | const gzipTransform = createGzip();
66 | const handleStreamError = (err) => {
67 | console.error(err);
68 | fileStream.destroy();
69 | gzipTransform.destroy();
70 | res.statusCode = 500;
71 | };
72 | fileStream
73 | .on("error", handleStreamError)
74 | .pipe(gzipTransform)
75 | .on("error", handleStreamError)
76 | .pipe(res);
77 | });
78 |
79 | server.listen(port, host, () => {
80 | console.log(`Web server running at http://${host}:${port}/`);
81 | });
82 |
--------------------------------------------------------------------------------
/06-www/12-w3-index.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import mimeMap from "./media-types.mjs";
3 | import { extname } from "path";
4 | import { log } from "./logger.mjs";
5 | import { createGzip } from "zlib";
6 | import { hrtime } from "process";
7 | import {
8 | generateLogString,
9 | tryOpenFile,
10 | isStringTrue,
11 | exitIfNotDir,
12 | } from "./w3-utils.mjs";
13 |
14 | const host = process.env.WEB_HOST || "127.0.0.1";
15 | const port = process.env.WEB_PORT || 3000;
16 | const root = process.env.WEB_ROOT || "files";
17 | const index = isStringTrue(process.env.WEB_INDEX) || false;
18 |
19 | await exitIfNotDir(root);
20 |
21 | const server = http.createServer();
22 | server.on("request", async (req, res) => {
23 | const startTime = hrtime.bigint();
24 |
25 | const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"];
26 | if (!isGET && !isHEAD) {
27 | res.statusCode = 405;
28 | res.end();
29 | return;
30 | }
31 |
32 | const { pathname } = new URL(req.url, `http://${req.headers.host}`);
33 | const localPath = `${root}${pathname}`;
34 |
35 | res.on("finish", () => {
36 | log(generateLogString(req, res, pathname, startTime));
37 | });
38 |
39 | const { found, fh, fileStat } = await tryOpenFile(localPath, index);
40 | if (!found) {
41 | res.statusCode = 404;
42 | res.end();
43 | return;
44 | }
45 |
46 | const mimeType = mimeMap.get(extname(pathname));
47 | if (mimeType) {
48 | res.setHeader("Content-Type", mimeType);
49 | }
50 |
51 | if (isHEAD) {
52 | res.setHeader("Content-Length", fileStat.size);
53 | res.statusCode = 200;
54 | res.end();
55 | await fh.close();
56 | return;
57 | }
58 |
59 | res.setHeader("Content-Encoding", "gzip");
60 | const fileStream = fh.createReadStream();
61 | const gzipTransform = createGzip();
62 | const handleStreamError = (err) => {
63 | console.error(err);
64 | fileStream.destroy();
65 | gzipTransform.destroy();
66 | res.statusCode = 500;
67 | };
68 | fileStream
69 | .on("error", handleStreamError)
70 | .pipe(gzipTransform)
71 | .on("error", handleStreamError)
72 | .pipe(res);
73 | });
74 |
75 | server.listen(port, host, () => {
76 | console.log(`Web server running at http://${host}:${port}/`);
77 | });
78 |
--------------------------------------------------------------------------------
/06-www/files/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lifeisfoo/node-js/5d28f338f32b0035ad8426939c2c92c4d936f8ff/06-www/files/favicon.ico
--------------------------------------------------------------------------------
/06-www/files/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Welcome to Node.js web server
6 |
7 |
8 | Welcome to Node.js web server!
9 | If you see this page, the Node.js web server is working.
10 |
11 |
12 | For a rich HTML page see
13 | the kitchen sink .
14 | This server is also able to serve
15 | funny images .
16 |
17 |
18 | Thank you for using Node.js web server.
19 |
20 |
21 |
--------------------------------------------------------------------------------
/06-www/files/kitchen-sink.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | My title
14 |
15 |
16 |
17 |
45 |
46 |
47 |
48 |
49 | Section Heading
50 | Section Subheading
51 |
52 |
53 | Card heading
54 | Card content*
55 | *with small content
56 |
57 |
61 |
62 | Card heading
63 | Card content
64 |
65 |
66 |
67 |
68 |
69 | "Quote"
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Col A
80 | Col B
81 | Col C
82 |
83 |
84 |
85 | Row 1
86 | Cell A1
87 | Cell B1
88 | Cell C1
89 |
90 |
91 | Row 2
92 | Cell A2
93 | Cell B2
94 | Cell C2
95 |
96 |
97 |
98 |
99 |
100 | Left-aligned header
101 | Left-aligned paragraph
102 |
103 | Article callout
104 |
105 |
106 | List item 1
107 | List item 2
108 |
109 |
110 |
114 | Image caption
115 |
116 |
117 |
118 |
119 |
120 | Expandable title
121 | Revealed content
122 |
123 |
124 | Another expandable title
125 | More revealed content
126 |
127 |
128 |
Inline code
snippets
129 |
130 |
131 | // preformatted code block
132 |
133 |
134 |
135 |
136 |
159 |
160 |
161 |
162 |
163 | Contact info
164 |
165 |
166 |
167 |
168 |
--------------------------------------------------------------------------------
/06-www/files/selfie.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lifeisfoo/node-js/5d28f338f32b0035ad8426939c2c92c4d936f8ff/06-www/files/selfie.jpg
--------------------------------------------------------------------------------
/06-www/files/style.css:
--------------------------------------------------------------------------------
1 | /* MVP.css v1.14 - https://github.com/andybrewer/mvp */
2 |
3 | :root {
4 | --active-brightness: 0.85;
5 | --border-radius: 5px;
6 | --box-shadow: 2px 2px 10px;
7 | --color-accent: #118bee15;
8 | --color-bg: #fff;
9 | --color-bg-secondary: #e9e9e9;
10 | --color-link: #118bee;
11 | --color-secondary: #920de9;
12 | --color-secondary-accent: #920de90b;
13 | --color-shadow: #f4f4f4;
14 | --color-table: #118bee;
15 | --color-text: #000;
16 | --color-text-secondary: #999;
17 | --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
18 | --hover-brightness: 1.2;
19 | --justify-important: center;
20 | --justify-normal: left;
21 | --line-height: 1.5;
22 | --width-card: 285px;
23 | --width-card-medium: 460px;
24 | --width-card-wide: 800px;
25 | --width-content: 1080px;
26 | }
27 |
28 | @media (prefers-color-scheme: dark) {
29 | :root[color-mode="user"] {
30 | --color-accent: #0097fc4f;
31 | --color-bg: #333;
32 | --color-bg-secondary: #555;
33 | --color-link: #0097fc;
34 | --color-secondary: #e20de9;
35 | --color-secondary-accent: #e20de94f;
36 | --color-shadow: #bbbbbb20;
37 | --color-table: #0097fc;
38 | --color-text: #f7f7f7;
39 | --color-text-secondary: #aaa;
40 | }
41 | }
42 |
43 | html {
44 | scroll-behavior: smooth;
45 | }
46 |
47 | @media (prefers-reduced-motion: reduce) {
48 | html {
49 | scroll-behavior: auto;
50 | }
51 | }
52 |
53 | /* Layout */
54 | article aside {
55 | background: var(--color-secondary-accent);
56 | border-left: 4px solid var(--color-secondary);
57 | padding: 0.01rem 0.8rem;
58 | }
59 |
60 | body {
61 | background: var(--color-bg);
62 | color: var(--color-text);
63 | font-family: var(--font-family);
64 | line-height: var(--line-height);
65 | margin: 0;
66 | overflow-x: hidden;
67 | padding: 0;
68 | }
69 |
70 | footer,
71 | header,
72 | main {
73 | margin: 0 auto;
74 | max-width: var(--width-content);
75 | padding: 3rem 1rem;
76 | }
77 |
78 | hr {
79 | background-color: var(--color-bg-secondary);
80 | border: none;
81 | height: 1px;
82 | margin: 4rem 0;
83 | width: 100%;
84 | }
85 |
86 | section {
87 | display: flex;
88 | flex-wrap: wrap;
89 | justify-content: var(--justify-important);
90 | }
91 |
92 | section img,
93 | article img {
94 | max-width: 100%;
95 | }
96 |
97 | section pre {
98 | overflow: auto;
99 | }
100 |
101 | section aside {
102 | border: 1px solid var(--color-bg-secondary);
103 | border-radius: var(--border-radius);
104 | box-shadow: var(--box-shadow) var(--color-shadow);
105 | margin: 1rem;
106 | padding: 1.25rem;
107 | width: var(--width-card);
108 | }
109 |
110 | section aside:hover {
111 | box-shadow: var(--box-shadow) var(--color-bg-secondary);
112 | }
113 |
114 | [hidden] {
115 | display: none;
116 | }
117 |
118 | /* Headers */
119 | article header,
120 | div header,
121 | main header {
122 | padding-top: 0;
123 | }
124 |
125 | header {
126 | text-align: var(--justify-important);
127 | }
128 |
129 | header a b,
130 | header a em,
131 | header a i,
132 | header a strong {
133 | margin-left: 0.5rem;
134 | margin-right: 0.5rem;
135 | }
136 |
137 | header nav img {
138 | margin: 1rem 0;
139 | }
140 |
141 | section header {
142 | padding-top: 0;
143 | width: 100%;
144 | }
145 |
146 | /* Nav */
147 | nav {
148 | align-items: center;
149 | display: flex;
150 | font-weight: bold;
151 | justify-content: space-between;
152 | margin-bottom: 7rem;
153 | }
154 |
155 | nav ul {
156 | list-style: none;
157 | padding: 0;
158 | }
159 |
160 | nav ul li {
161 | display: inline-block;
162 | margin: 0 0.5rem;
163 | position: relative;
164 | text-align: left;
165 | }
166 |
167 | /* Nav Dropdown */
168 | nav ul li:hover ul {
169 | display: block;
170 | }
171 |
172 | nav ul li ul {
173 | background: var(--color-bg);
174 | border: 1px solid var(--color-bg-secondary);
175 | border-radius: var(--border-radius);
176 | box-shadow: var(--box-shadow) var(--color-shadow);
177 | display: none;
178 | height: auto;
179 | left: -2px;
180 | padding: .5rem 1rem;
181 | position: absolute;
182 | top: 1.7rem;
183 | white-space: nowrap;
184 | width: auto;
185 | z-index: 1;
186 | }
187 |
188 | nav ul li ul::before {
189 | /* fill gap above to make mousing over them easier */
190 | content: "";
191 | position: absolute;
192 | left: 0;
193 | right: 0;
194 | top: -0.5rem;
195 | height: 0.5rem;
196 | }
197 |
198 | nav ul li ul li,
199 | nav ul li ul li a {
200 | display: block;
201 | }
202 |
203 | /* Typography */
204 | code,
205 | samp {
206 | background-color: var(--color-accent);
207 | border-radius: var(--border-radius);
208 | color: var(--color-text);
209 | display: inline-block;
210 | margin: 0 0.1rem;
211 | padding: 0 0.5rem;
212 | }
213 |
214 | details {
215 | margin: 1.3rem 0;
216 | }
217 |
218 | details summary {
219 | font-weight: bold;
220 | cursor: pointer;
221 | }
222 |
223 | h1,
224 | h2,
225 | h3,
226 | h4,
227 | h5,
228 | h6 {
229 | line-height: var(--line-height);
230 | text-wrap: balance;
231 | }
232 |
233 | mark {
234 | padding: 0.1rem;
235 | }
236 |
237 | ol li,
238 | ul li {
239 | padding: 0.2rem 0;
240 | }
241 |
242 | p {
243 | margin: 0.75rem 0;
244 | padding: 0;
245 | width: 100%;
246 | }
247 |
248 | pre {
249 | margin: 1rem 0;
250 | max-width: var(--width-card-wide);
251 | padding: 1rem 0;
252 | }
253 |
254 | pre code,
255 | pre samp {
256 | display: block;
257 | max-width: var(--width-card-wide);
258 | padding: 0.5rem 2rem;
259 | white-space: pre-wrap;
260 | }
261 |
262 | small {
263 | color: var(--color-text-secondary);
264 | }
265 |
266 | sup {
267 | background-color: var(--color-secondary);
268 | border-radius: var(--border-radius);
269 | color: var(--color-bg);
270 | font-size: xx-small;
271 | font-weight: bold;
272 | margin: 0.2rem;
273 | padding: 0.2rem 0.3rem;
274 | position: relative;
275 | top: -2px;
276 | }
277 |
278 | /* Links */
279 | a {
280 | color: var(--color-link);
281 | display: inline-block;
282 | font-weight: bold;
283 | text-decoration: underline;
284 | }
285 |
286 | a:active {
287 | filter: brightness(var(--active-brightness));
288 | }
289 |
290 | a:hover {
291 | filter: brightness(var(--hover-brightness));
292 | }
293 |
294 | a b,
295 | a em,
296 | a i,
297 | a strong,
298 | button,
299 | input[type="submit"] {
300 | border-radius: var(--border-radius);
301 | display: inline-block;
302 | font-size: medium;
303 | font-weight: bold;
304 | line-height: var(--line-height);
305 | margin: 0.5rem 0;
306 | padding: 1rem 2rem;
307 | }
308 |
309 | button,
310 | input[type="submit"] {
311 | font-family: var(--font-family);
312 | }
313 |
314 | button:active,
315 | input[type="submit"]:active {
316 | filter: brightness(var(--active-brightness));
317 | }
318 |
319 | button:hover,
320 | input[type="submit"]:hover {
321 | cursor: pointer;
322 | filter: brightness(var(--hover-brightness));
323 | }
324 |
325 | a b,
326 | a strong,
327 | button,
328 | input[type="submit"] {
329 | background-color: var(--color-link);
330 | border: 2px solid var(--color-link);
331 | color: var(--color-bg);
332 | }
333 |
334 | a em,
335 | a i {
336 | border: 2px solid var(--color-link);
337 | border-radius: var(--border-radius);
338 | color: var(--color-link);
339 | display: inline-block;
340 | padding: 1rem 2rem;
341 | }
342 |
343 | article aside a {
344 | color: var(--color-secondary);
345 | }
346 |
347 | /* Images */
348 | figure {
349 | margin: 0;
350 | padding: 0;
351 | }
352 |
353 | figure img {
354 | max-width: 100%;
355 | }
356 |
357 | figure figcaption {
358 | color: var(--color-text-secondary);
359 | }
360 |
361 | /* Forms */
362 | button:disabled,
363 | input:disabled {
364 | background: var(--color-bg-secondary);
365 | border-color: var(--color-bg-secondary);
366 | color: var(--color-text-secondary);
367 | cursor: not-allowed;
368 | }
369 |
370 | button[disabled]:hover,
371 | input[type="submit"][disabled]:hover {
372 | filter: none;
373 | }
374 |
375 | form {
376 | border: 1px solid var(--color-bg-secondary);
377 | border-radius: var(--border-radius);
378 | box-shadow: var(--box-shadow) var(--color-shadow);
379 | display: block;
380 | max-width: var(--width-card-wide);
381 | min-width: var(--width-card);
382 | padding: 1.5rem;
383 | text-align: var(--justify-normal);
384 | }
385 |
386 | form header {
387 | margin: 1.5rem 0;
388 | padding: 1.5rem 0;
389 | }
390 |
391 | input,
392 | label,
393 | select,
394 | textarea {
395 | display: block;
396 | font-size: inherit;
397 | max-width: var(--width-card-wide);
398 | }
399 |
400 | input[type="checkbox"],
401 | input[type="radio"] {
402 | display: inline-block;
403 | }
404 |
405 | input[type="checkbox"]+label,
406 | input[type="radio"]+label {
407 | display: inline-block;
408 | font-weight: normal;
409 | position: relative;
410 | top: 1px;
411 | }
412 |
413 | input[type="range"] {
414 | padding: 0.4rem 0;
415 | }
416 |
417 | input,
418 | select,
419 | textarea {
420 | border: 1px solid var(--color-bg-secondary);
421 | border-radius: var(--border-radius);
422 | margin-bottom: 1rem;
423 | padding: 0.4rem 0.8rem;
424 | }
425 |
426 | input[type="text"],
427 | textarea {
428 | width: calc(100% - 1.6rem);
429 | }
430 |
431 | input[readonly],
432 | textarea[readonly] {
433 | background-color: var(--color-bg-secondary);
434 | }
435 |
436 | label {
437 | font-weight: bold;
438 | margin-bottom: 0.2rem;
439 | }
440 |
441 | /* Popups */
442 | dialog {
443 | border: 1px solid var(--color-bg-secondary);
444 | border-radius: var(--border-radius);
445 | box-shadow: var(--box-shadow) var(--color-shadow);
446 | position: fixed;
447 | top: 50%;
448 | left: 50%;
449 | transform: translate(-50%, -50%);
450 | width: 50%;
451 | z-index: 999;
452 | }
453 |
454 | /* Tables */
455 | table {
456 | border: 1px solid var(--color-bg-secondary);
457 | border-radius: var(--border-radius);
458 | border-spacing: 0;
459 | display: inline-block;
460 | max-width: 100%;
461 | overflow-x: auto;
462 | padding: 0;
463 | white-space: nowrap;
464 | }
465 |
466 | table td,
467 | table th,
468 | table tr {
469 | padding: 0.4rem 0.8rem;
470 | text-align: var(--justify-important);
471 | }
472 |
473 | table thead {
474 | background-color: var(--color-table);
475 | border-collapse: collapse;
476 | border-radius: var(--border-radius);
477 | color: var(--color-bg);
478 | margin: 0;
479 | padding: 0;
480 | }
481 |
482 | table thead th:first-child {
483 | border-top-left-radius: var(--border-radius);
484 | }
485 |
486 | table thead th:last-child {
487 | border-top-right-radius: var(--border-radius);
488 | }
489 |
490 | table thead th:first-child,
491 | table tr td:first-child {
492 | text-align: var(--justify-normal);
493 | }
494 |
495 | table tr:nth-child(even) {
496 | background-color: var(--color-accent);
497 | }
498 |
499 | /* Quotes */
500 | blockquote {
501 | display: block;
502 | font-size: x-large;
503 | line-height: var(--line-height);
504 | margin: 1rem auto;
505 | max-width: var(--width-card-medium);
506 | padding: 1.5rem 1rem;
507 | text-align: var(--justify-important);
508 | }
509 |
510 | blockquote footer {
511 | color: var(--color-text-secondary);
512 | display: block;
513 | font-size: small;
514 | line-height: var(--line-height);
515 | padding: 1.5rem 0;
516 | }
517 |
518 | /* Scrollbars */
519 | * {
520 | scrollbar-width: thin;
521 | scrollbar-color: rgb(202, 202, 232) auto;
522 | }
523 |
524 | *::-webkit-scrollbar {
525 | width: 5px;
526 | height: 5px;
527 | }
528 |
529 | *::-webkit-scrollbar-track {
530 | background: transparent;
531 | }
532 |
533 | *::-webkit-scrollbar-thumb {
534 | background-color: rgb(202, 202, 232);
535 | border-radius: 10px;
536 | }
537 |
--------------------------------------------------------------------------------
/06-www/logger-unsafe.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 |
3 | const logFh = await fs.open("./access.log", "a");
4 |
5 | async function log(msg) {
6 | await logFh.write(`${msg}\n`);
7 | }
8 |
9 | export { log };
10 |
--------------------------------------------------------------------------------
/06-www/logger.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 |
3 | const logFh = await fs.open("./access.log", "a");
4 | const logStream = logFh.createWriteStream();
5 |
6 | const log = function (msg) {
7 | console.log(msg);
8 | logStream.write(`${msg}\n`);
9 | };
10 |
11 | export { log };
12 |
--------------------------------------------------------------------------------
/06-www/media-types.mjs:
--------------------------------------------------------------------------------
1 | const types = {
2 | "text/html": ["html", "htm"],
3 | "text/plain": ["txt", "text"],
4 | "text/css": ["css"],
5 | "application/javascript": ["js", "mjs"],
6 | "application/json": ["json"],
7 | "image/jpeg": ["jpeg", "jpg"],
8 | "image/x-icon": ["ico"],
9 | "image/avif": ["avif"],
10 | "audio/mpeg": ["mpga", "mp2", "mp2a", "mp3", "m2a", "m3a"],
11 | "video/mp4": ["mp4", "mp4v", "mpg4"],
12 | "video/mpeg": ["mpeg", "mpg", "mpe", "m1v", "m2v"],
13 | "video/ogg": ["ogv"],
14 | "video/quicktime": ["qt", "mov"],
15 | "video/x-matroska": ["mkv", "mk3d", "mks"],
16 | "video/x-msvideo": ["avi"],
17 | };
18 | const extMap = new Map();
19 |
20 | for (let [type, extensions] of Object.entries(types)) {
21 | for (let extension of extensions) {
22 | extMap.set(`.${extension}`, type);
23 | }
24 | }
25 |
26 | export default extMap;
27 |
--------------------------------------------------------------------------------
/06-www/w3-utils.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 | import { hrtime } from "process";
3 |
4 | async function exitIfNotDir(path) {
5 | try {
6 | const pathStat = await fs.stat(path);
7 | if (!pathStat.isDirectory()) {
8 | console.error(`${path} is not a directory.`);
9 | process.exit(1);
10 | }
11 | } catch {
12 | console.error(`${path} does not exists or cannot be opened.`);
13 | process.exit(1);
14 | }
15 | }
16 |
17 | function isStringTrue(str) {
18 | if (str === "true") {
19 | return true;
20 | }
21 | return false;
22 | }
23 |
24 | function hasTrailingSlash(path) {
25 | return path[path.length - 1] === "/";
26 | }
27 |
28 | async function tryOpenFile(path, index) {
29 | let file = path;
30 | if (hasTrailingSlash(path)) {
31 | if (index) {
32 | file = `${path}index.html`;
33 | } else {
34 | return { found: false };
35 | }
36 | }
37 |
38 | let fh;
39 | try {
40 | fh = await fs.open(file);
41 | } catch (e) {
42 | console.error(e);
43 | return { found: false };
44 | }
45 |
46 | const fileStat = await fh.stat();
47 | if (fileStat.isDirectory()) {
48 | //spiegare caso eccezionale - così agganciarsi ai tanti controlli di sicurezza da fare nel caso reale
49 | await fh.close();
50 | return { found: false };
51 | }
52 | return { found: true, fh, fileStat };
53 | }
54 |
55 | function generateLogString(req, res, pathname, startTime) {
56 | const endTime = hrtime.bigint();
57 | const duration = (endTime - startTime) / BigInt(1e6);
58 | const timestamp = new Date().toISOString();
59 | const ip = req.socket.remoteAddress;
60 | const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`;
61 | return `${ip} - ${timestamp} ${httpDetails} - ${duration}ms`;
62 | }
63 |
64 | export { exitIfNotDir, isStringTrue, tryOpenFile, generateLogString };
65 |
--------------------------------------------------------------------------------
/07-eloop/01-linear-prime.mjs:
--------------------------------------------------------------------------------
1 | import { isPrime } from "./cpu-intensive.mjs";
2 |
3 | console.log("Script started");
4 | console.log(`Is it prime? ${isPrime(2971215073)}`);
5 | console.log("Script ended");
6 |
--------------------------------------------------------------------------------
/07-eloop/02-callback-prime.mjs:
--------------------------------------------------------------------------------
1 | import { PRIME_BIG, isPrime } from "./cpu-intensive.mjs";
2 |
3 | function calcPrime(num, cb) {
4 | const isNumPrime = isPrime(num);
5 | cb(isNumPrime);
6 | }
7 | console.log("Script started");
8 |
9 | calcPrime(PRIME_BIG, (res) => {
10 | console.log(`${PRIME_BIG} is prime? ${res}`);
11 | });
12 |
13 | console.log("Script ended");
14 |
--------------------------------------------------------------------------------
/07-eloop/03-callback-timeout.mjs:
--------------------------------------------------------------------------------
1 | import { PRIME_BIG, isPrime } from "./cpu-intensive.mjs";
2 |
3 | function calcPrime(num, cb) {
4 | setTimeout(() => {
5 | const isNumPrime = isPrime(num);
6 | cb(isNumPrime);
7 | }, 1000);
8 | }
9 | console.log("Script started");
10 |
11 | calcPrime(PRIME_BIG, (res) => {
12 | console.log(`${PRIME_BIG} is prime? ${res}`);
13 | });
14 |
15 | console.log("Script ended (?)");
16 |
--------------------------------------------------------------------------------
/07-eloop/04-timers-poll-check.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { isPrime } from "./cpu-intensive.mjs";
3 |
4 | console.log("running the script...");
5 |
6 | fs.readFile("./number.txt", { encoding: "utf-8" }, (err, data) => {
7 | const numToCheck = Number.parseInt(data);
8 | console.log(`Number to check: ${numToCheck}`);
9 |
10 | const primeRes = isPrime(numToCheck);
11 |
12 | setTimeout(() => {
13 | console.log(`[timeout] ${numToCheck} is prime? ${primeRes}`);
14 | }, 0);
15 | setImmediate(() => {
16 | console.log(`[immediate] ${numToCheck} is prime? ${primeRes}`);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/07-eloop/05-draining-queues.mjs:
--------------------------------------------------------------------------------
1 | setTimeout(() => {
2 | console.log(`[timeout] 1`);
3 | }, 0);
4 | setImmediate(() => {
5 | console.log(`[immediate] 1`);
6 | });
7 | setTimeout(() => {
8 | console.log(`[timeout] 2`);
9 | }, 0);
10 | setImmediate(() => {
11 | console.log(`[immediate] 2`);
12 | });
13 | setTimeout(() => {
14 | console.log(`[timeout] 3`);
15 | }, 0);
16 | setImmediate(() => {
17 | console.log(`[immediate] 3`);
18 | });
19 |
--------------------------------------------------------------------------------
/07-eloop/06-scheduling-timers.mjs:
--------------------------------------------------------------------------------
1 | setTimeout(() => {
2 | console.log(`[timeout 0]`);
3 | }, 0);
4 | setImmediate(() => {
5 | console.log(`[immediate 0]`);
6 | setTimeout(() => {
7 | console.log(`[timeout] scheduled from immediate 0`);
8 | }, 0);
9 | setImmediate(() => {
10 | console.log(`[immediate] scheduled from immediate 0`);
11 | });
12 | let i = 0;
13 | while (i < 10000000) {
14 | i++;
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/07-eloop/07-microtasks.mjs:
--------------------------------------------------------------------------------
1 | setImmediate(() => {
2 | console.log("first immediate");
3 | Promise.resolve()
4 | .then(() => {
5 | console.log("then 1");
6 | return;
7 | })
8 | .then(() => {
9 | console.log("then 2");
10 | });
11 | queueMicrotask(() => {
12 | console.log("queueMicrotask");
13 | });
14 | });
15 | setImmediate(() => {
16 | console.log("last immediate");
17 | });
18 |
--------------------------------------------------------------------------------
/07-eloop/08-next-tick.mjs:
--------------------------------------------------------------------------------
1 | setImmediate(() => {
2 | console.log("first immediate");
3 | Promise.resolve()
4 | .then(() => {
5 | console.log("then 1");
6 | return;
7 | })
8 | .then(() => {
9 | console.log("then 2");
10 | });
11 | queueMicrotask(() => {
12 | console.log("queueMicrotask");
13 | });
14 | process.nextTick(() => {
15 | console.log("next tick");
16 | });
17 | });
18 | setImmediate(() => {
19 | console.log("last immediate");
20 | });
21 |
--------------------------------------------------------------------------------
/07-eloop/09-partitioned.mjs:
--------------------------------------------------------------------------------
1 | import { PRIME_MED, isPrimeAsync } from "./cpu-intensive.mjs";
2 | import { hrtime } from "process";
3 |
4 | let lastTime = hrtime.bigint();
5 | const interval = setInterval(() => {
6 | const now = hrtime.bigint();
7 | const duration = (now - lastTime) / BigInt(1e6);
8 | console.log(`[interval] ${duration}ms passed since last one`);
9 | }, 500);
10 |
11 | isPrimeAsync(PRIME_MED, function (res) {
12 | console.log(`${PRIME_MED} is prime? ${res}`);
13 | clearInterval(interval);
14 | });
15 |
--------------------------------------------------------------------------------
/07-eloop/10-http-prime-blocking.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import { isPrime } from "./cpu-intensive.mjs";
3 | import { generateLogString } from "./eloop-utils.mjs";
4 | import { hrtime } from "process";
5 |
6 | const server = http.createServer();
7 | server.on("request", async (req, res) => {
8 | const startTime = hrtime.bigint();
9 |
10 | const { searchParams, pathname } = new URL(
11 | req.url,
12 | `http://${req.headers.host}`
13 | );
14 | const num = parseInt(searchParams.get("num"));
15 | res.on("finish", () => {
16 | console.log(generateLogString(req, res, pathname, startTime));
17 | });
18 |
19 | const primeRes = isPrime(num);
20 |
21 | res.end(`${num} is prime? ${primeRes}`);
22 | });
23 |
24 | server.listen(3000, "127.0.0.1", () => {
25 | console.log(`Web server running`);
26 | });
27 |
--------------------------------------------------------------------------------
/07-eloop/11-http-prime-partitioned.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import { isPrimeAsync } from "./cpu-intensive.mjs";
3 | import { generateLogString } from "./eloop-utils.mjs";
4 | import { hrtime } from "process";
5 |
6 | const server = http.createServer();
7 | server.on("request", async (req, res) => {
8 | const startTime = hrtime.bigint();
9 |
10 | const { searchParams, pathname } = new URL(
11 | req.url,
12 | `http://${req.headers.host}`
13 | );
14 | const num = parseInt(searchParams.get("num"));
15 | res.on("finish", () => {
16 | console.log(generateLogString(req, res, pathname, startTime));
17 | });
18 |
19 | isPrimeAsync(num, (primeRes) => {
20 | res.end(`${num} is prime? ${primeRes}`);
21 | });
22 | });
23 |
24 | server.listen(3000, "127.0.0.1", () => {
25 | console.log(`Web server running`);
26 | });
27 |
--------------------------------------------------------------------------------
/07-eloop/12-child-exec-file.mjs:
--------------------------------------------------------------------------------
1 | import { execFile } from "child_process";
2 |
3 | execFile("node", ["isprime.mjs", 3], (error, stdout, stderr) => {
4 | if (error) {
5 | console.log("The number is NOT prime");
6 | } else {
7 | console.log("The number is prime");
8 | }
9 | console.log(`stdout: ${stdout}`);
10 | console.log(`stderr: ${stderr}`);
11 | });
12 |
--------------------------------------------------------------------------------
/07-eloop/13-child-exec.mjs:
--------------------------------------------------------------------------------
1 | import { exec } from "child_process";
2 |
3 | exec("./isprime.mjs 3", (error, stdout, stderr) => {
4 | if (error) {
5 | console.log("The number is NOT prime");
6 | } else {
7 | console.log("The number is prime");
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/07-eloop/14-http-prime-child-node.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import { generateLogString } from "./eloop-utils.mjs";
3 | import { hrtime } from "process";
4 | import { exec } from "child_process";
5 |
6 | const server = http.createServer();
7 | server.on("request", async (req, res) => {
8 | const startTime = hrtime.bigint();
9 |
10 | const { searchParams, pathname } = new URL(
11 | req.url,
12 | `http://${req.headers.host}`
13 | );
14 | const num = parseInt(searchParams.get("num"));
15 | res.on("finish", () => {
16 | console.log(generateLogString(req, res, pathname, startTime));
17 | });
18 |
19 | exec(`node isprime.mjs ${num}`, (error, stdout, stderr) => {
20 | if (error) {
21 | res.end(`${num} is prime? false`);
22 | } else {
23 | res.end(`${num} is prime? true`);
24 | }
25 | });
26 | });
27 |
28 | server.listen(3000, "127.0.0.1", () => {
29 | console.log(`Web server running`);
30 | });
31 |
--------------------------------------------------------------------------------
/07-eloop/15-http-prime-child-c.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import { generateLogString } from "./eloop-utils.mjs";
3 | import { hrtime } from "process";
4 | import { exec } from "child_process";
5 |
6 | const server = http.createServer();
7 | server.on("request", async (req, res) => {
8 | const startTime = hrtime.bigint();
9 |
10 | const { searchParams, pathname } = new URL(
11 | req.url,
12 | `http://${req.headers.host}`
13 | );
14 | const num = parseInt(searchParams.get("num"));
15 | res.on("finish", () => {
16 | console.log(generateLogString(req, res, pathname, startTime));
17 | });
18 |
19 | exec(`./isprime ${num}`, (error, stdout, stderr) => {
20 | if (error) {
21 | res.end(`${num} is prime? false`);
22 | } else {
23 | res.end(`${num} is prime? true`);
24 | }
25 | });
26 | });
27 |
28 | server.listen(3000, "127.0.0.1", () => {
29 | console.log(`Web server running`);
30 | });
31 |
--------------------------------------------------------------------------------
/07-eloop/16-child-spawn.mjs:
--------------------------------------------------------------------------------
1 | import { spawn } from "child_process";
2 |
3 | const ls = spawn("ls");
4 |
5 | ls.stdout.on("data", (data) => {
6 | console.log(`ls stdout ${data}`);
7 | });
8 | ls.stderr.on("data", (data) => {
9 | console.error(`ls stderr: ${data}`);
10 | });
11 | ls.on("close", (code) => {
12 | if (code !== 0) {
13 | console.log(`ls process exited with code ${code}`);
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/07-eloop/17-child-spawn-pipe.mjs:
--------------------------------------------------------------------------------
1 | import { spawn } from "child_process";
2 |
3 | const ls = spawn("ls", ["*.mjs"], { shell: true }); // https://nodejs.org/api/deprecations.html#DEP0190
4 | const grep = spawn("grep", ["http"]);
5 |
6 | ls.stdout.on("data", (data) => {
7 | grep.stdin.write(data);
8 | });
9 |
10 | ls.stderr.on("data", (data) => {
11 | console.error(`ls stderr: ${data}`);
12 | });
13 |
14 | ls.on("close", (code) => {
15 | if (code !== 0) {
16 | console.log(`ls process exited with code ${code}`);
17 | }
18 | grep.stdin.end();
19 | });
20 |
21 | grep.stdout.on("data", (data) => {
22 | console.log(data.toString());
23 | });
24 |
25 | grep.stderr.on("data", (data) => {
26 | console.error(`grep stderr: ${data}`);
27 | });
28 |
29 | grep.on("close", (code) => {
30 | if (code !== 0) {
31 | console.log(`grep process exited with code ${code}`);
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/07-eloop/18-http-prime-child-spawn.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import { generateLogString } from "./eloop-utils.mjs";
3 | import { hrtime } from "process";
4 | import { spawn } from "child_process";
5 |
6 | const server = http.createServer();
7 | server.on("request", async (req, res) => {
8 | const startTime = hrtime.bigint();
9 |
10 | const { searchParams, pathname } = new URL(
11 | req.url,
12 | `http://${req.headers.host}`
13 | );
14 | const num = parseInt(searchParams.get("num"));
15 | res.on("finish", () => {
16 | console.log(generateLogString(req, res, pathname, startTime));
17 | });
18 |
19 | const proc = spawn("node", ["isprime.mjs", num]);
20 | proc.on("close", (code) => {
21 | if (code !== 0) {
22 | res.end(`${num} is prime? false`);
23 | } else {
24 | res.end(`${num} is prime? true`);
25 | }
26 | });
27 | });
28 |
29 | server.listen(3000, "127.0.0.1", () => {
30 | console.log(`Web server running`);
31 | });
32 |
--------------------------------------------------------------------------------
/07-eloop/19-child-fork.mjs:
--------------------------------------------------------------------------------
1 | import { fork } from "child_process";
2 | import { PRIME_HUGE } from "./cpu-intensive.mjs";
3 |
4 | const interval = setInterval(() => {
5 | console.log(
6 | `${new Date().toISOString()} - isprime() is still running in another process..`
7 | );
8 | }, 1000);
9 |
10 | const child = fork("isprime-child.mjs");
11 | child.send(PRIME_HUGE);
12 | child.on("message", (result) => {
13 | console.log(`${PRIME_HUGE} is prime? ${result}`);
14 | clearInterval(interval);
15 | });
16 |
--------------------------------------------------------------------------------
/07-eloop/20-http-prime-child-fork.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import { generateLogString } from "./eloop-utils.mjs";
3 | import { hrtime } from "process";
4 | import { fork } from "child_process";
5 |
6 | const server = http.createServer();
7 | server.on("request", async (req, res) => {
8 | const startTime = hrtime.bigint();
9 |
10 | const { searchParams, pathname } = new URL(
11 | req.url,
12 | `http://${req.headers.host}`
13 | );
14 | const num = parseInt(searchParams.get("num"));
15 | res.on("finish", () => {
16 | console.log(generateLogString(req, res, pathname, startTime));
17 | });
18 |
19 | const child = fork("isprime-child.mjs");
20 | child.send(num);
21 | child.on("message", (result) => {
22 | if (result) {
23 | res.end(`${num} is prime? true`);
24 | } else {
25 | res.end(`${num} is prime? false`);
26 | }
27 | });
28 | });
29 |
30 | server.listen(3000, "127.0.0.1", () => {
31 | console.log(`Web server running`);
32 | });
33 |
--------------------------------------------------------------------------------
/07-eloop/21-self-fork.mjs:
--------------------------------------------------------------------------------
1 | import { PRIME_HUGE, isPrime } from "./cpu-intensive.mjs";
2 | import { fork } from "child_process";
3 |
4 | if (process.argv[2] === "child") {
5 | process.on("message", (msg) => {
6 | const result = isPrime(msg);
7 | process.send(result);
8 | process.exit();
9 | });
10 | } else {
11 | const interval = setInterval(() => {
12 | console.log(
13 | `${new Date().toISOString()} - isprime() is still running in another process..`
14 | );
15 | }, 1000);
16 | const child = fork("21-self-fork.mjs", ["child"]);
17 | child.send(PRIME_HUGE);
18 | child.on("message", (result) => {
19 | console.log(`${PRIME_HUGE} is prime? ${result}`);
20 | clearInterval(interval);
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/07-eloop/22-http-prime-cluster.mjs:
--------------------------------------------------------------------------------
1 | import cluster from "cluster";
2 | import http from "http";
3 | import { availableParallelism } from "os";
4 | import process, { hrtime } from "process";
5 | import { generateLogString } from "./eloop-utils.mjs";
6 | import { isPrime } from "./cpu-intensive.mjs";
7 |
8 | const numCPUs = availableParallelism();
9 |
10 | if (cluster.isPrimary) {
11 | console.log(`Primary ${process.pid} is running`);
12 |
13 | for (let i = 0; i < numCPUs; i++) {
14 | cluster.fork();
15 | }
16 |
17 | cluster.on("exit", (worker, code) => {
18 | console.log(`worker ${worker.process.pid} exited with code ${code}`);
19 | });
20 | } else {
21 | const server = http.createServer();
22 | server.on("request", (req, res) => {
23 | const startTime = hrtime.bigint();
24 |
25 | const { searchParams, pathname } = new URL(
26 | req.url,
27 | `http://${req.headers.host}`
28 | );
29 | const num = parseInt(searchParams.get("num"));
30 | res.on("finish", () => {
31 | const logMsg = generateLogString(req, res, pathname, startTime);
32 | console.log(`${process.pid} - ${logMsg}`);
33 | });
34 |
35 | const primeRes = isPrime(num);
36 | res.end(`${num} is prime? ${primeRes}`);
37 | });
38 | server.listen(3000);
39 |
40 | console.log(`Worker ${process.pid} started`);
41 | }
42 |
--------------------------------------------------------------------------------
/07-eloop/23-worker-thread.mjs:
--------------------------------------------------------------------------------
1 | import { isMainThread, workerData, Worker, parentPort } from "worker_threads";
2 | import { PRIME_HUGE, isPrime } from "./cpu-intensive.mjs";
3 |
4 | if (isMainThread) {
5 | console.log(`Main thread pid is ${process.pid}`);
6 | const interval = setInterval(() => {
7 | console.log(
8 | `${new Date().toISOString()} - isprime() is still running in a worker thread..`
9 | );
10 | }, 1000);
11 | const worker = new Worker("./23-worker-thread.mjs", {
12 | workerData: { num: PRIME_HUGE },
13 | });
14 | worker.on("message", (result) => {
15 | console.log(`${PRIME_HUGE} is prime? ${result}`);
16 | clearInterval(interval);
17 | });
18 | } else {
19 | const result = isPrime(workerData.num);
20 | parentPort.postMessage(result);
21 | process.exit();
22 | }
23 |
--------------------------------------------------------------------------------
/07-eloop/24-http-prime-workers.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import process, { hrtime } from "process";
3 | import { generateLogString } from "./eloop-utils.mjs";
4 | import { isPrime } from "./cpu-intensive.mjs";
5 | import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
6 |
7 | if (isMainThread) {
8 | const server = http.createServer();
9 | server.on("request", (req, res) => {
10 | const startTime = hrtime.bigint();
11 |
12 | const { searchParams, pathname } = new URL(
13 | req.url,
14 | `http://${req.headers.host}`
15 | );
16 | const num = parseInt(searchParams.get("num"));
17 | res.on("finish", () => {
18 | console.log(generateLogString(req, res, pathname, startTime));
19 | });
20 |
21 | const worker = new Worker("./24-http-prime-workers.mjs", {
22 | workerData: { num: num },
23 | });
24 | worker.on("message", (result) => {
25 | res.end(`${num} is prime? ${result}`);
26 | });
27 | });
28 | server.listen(3000);
29 |
30 | console.log(`Main thread (server) started with pid ${process.pid}`);
31 | } else {
32 | const result = isPrime(workerData.num);
33 | parentPort.postMessage(result);
34 | process.exit();
35 | }
36 |
--------------------------------------------------------------------------------
/07-eloop/25-http-prime-workers-pool.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import process, { hrtime } from "process";
3 | import { generateLogString } from "./eloop-utils.mjs";
4 | import { isPrime } from "./cpu-intensive.mjs";
5 | import { Worker, isMainThread, parentPort } from "worker_threads";
6 | import { availableParallelism } from "os";
7 |
8 | const numCPUs = availableParallelism();
9 |
10 | if (isMainThread) {
11 | const workerPool = [];
12 | for (let i = 0; i < numCPUs; i++) {
13 | workerPool.push(new Worker("./25-http-prime-workers-pool.mjs"));
14 | }
15 | console.log(`Created ${workerPool.length} worker threads`);
16 |
17 | const server = http.createServer();
18 | server.on("request", (req, res) => {
19 | const startTime = hrtime.bigint();
20 |
21 | const { searchParams, pathname } = new URL(
22 | req.url,
23 | `http://${req.headers.host}`
24 | );
25 | const num = parseInt(searchParams.get("num"));
26 | res.on("finish", () => {
27 | console.log(generateLogString(req, res, pathname, startTime));
28 | });
29 |
30 | const wk = workerPool.pop();
31 | if (wk) {
32 | const listener = (result) => {
33 | wk.off("message", listener);
34 | workerPool.push(wk);
35 | res.end(`${num} is prime? ${result}`);
36 | };
37 | wk.on("message", listener);
38 | wk.postMessage(num);
39 | } else {
40 | res.statusCode = 503;
41 | res.end();
42 | }
43 | });
44 | server.listen(3000);
45 | console.log(`Main thread (server) started with pid ${process.pid}`);
46 | } else {
47 | parentPort.on("message", (msg) => {
48 | const result = isPrime(msg);
49 | parentPort.postMessage(result);
50 | });
51 | }
52 |
--------------------------------------------------------------------------------
/07-eloop/26-http-prime-workers-pool-2.mjs:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import process, { hrtime } from "process";
3 | import { generateLogString } from "./eloop-utils.mjs";
4 | import { Worker } from "worker_threads";
5 | import { availableParallelism } from "os";
6 |
7 | const numCPUs = availableParallelism();
8 |
9 | const workerPool = [];
10 | for (let i = 0; i < numCPUs; i++) {
11 | workerPool.push(new Worker("./isprime-worker.mjs"));
12 | }
13 | console.log(`Created ${workerPool.length} worker threads`);
14 |
15 | const server = http.createServer();
16 | server.on("request", (req, res) => {
17 | const startTime = hrtime.bigint();
18 |
19 | const { searchParams, pathname } = new URL(
20 | req.url,
21 | `http://${req.headers.host}`
22 | );
23 | const num = parseInt(searchParams.get("num"));
24 | res.on("finish", () => {
25 | console.log(generateLogString(req, res, pathname, startTime));
26 | });
27 |
28 | const wk = workerPool.pop();
29 | if (wk) {
30 | const listener = (result) => {
31 | wk.off("message", listener);
32 | workerPool.push(wk);
33 | res.end(`${num} is prime? ${result}`);
34 | };
35 | wk.on("message", listener);
36 | wk.postMessage(num);
37 | } else {
38 | res.statusCode = 503;
39 | res.end();
40 | }
41 | });
42 | server.listen(3000);
43 | console.log(`Main thread (server) started with pid ${process.pid}`);
44 |
--------------------------------------------------------------------------------
/07-eloop/cpu-intensive.mjs:
--------------------------------------------------------------------------------
1 | function isPrime(num) {
2 | if (num < 2) {
3 | return false;
4 | }
5 | for (let i = 2; i < num; i++) {
6 | if (num % i === 0) {
7 | return false;
8 | }
9 | }
10 | return true;
11 | }
12 |
13 | function factorial(n) {
14 | return n != 1 ? n * factorial(n - 1) : 1;
15 | }
16 |
17 | const PRIME_SMALL = 65537;
18 | const PRIME_MED = 999331;
19 | const PRIME_BIG = 39916801;
20 | const PRIME_HUGE = 2971215073;
21 | const PRIME_ENOR = 87178291199;
22 |
23 | function isPrimeAsync(num, cb) {
24 | process.nextTick(() => {
25 | if (num < 2) {
26 | return cb(false);
27 | }
28 | isPrimeRecursive(num, 2, cb);
29 | });
30 | }
31 |
32 | function isPrimeRecursive(n, i, cb) {
33 | if (i >= n) {
34 | return cb(true);
35 | }
36 | if (n % i === 0) {
37 | return cb(false);
38 | }
39 | setImmediate(isPrimeRecursive.bind(null, n, i + 1, cb));
40 | }
41 |
42 | export {
43 | isPrime,
44 | factorial,
45 | PRIME_SMALL,
46 | PRIME_MED,
47 | PRIME_BIG,
48 | PRIME_HUGE,
49 | PRIME_ENOR,
50 | isPrimeAsync,
51 | };
52 |
--------------------------------------------------------------------------------
/07-eloop/eloop-utils.mjs:
--------------------------------------------------------------------------------
1 | import { hrtime } from "process";
2 |
3 | function generateLogString(req, res, pathname, startTime) {
4 | const endTime = hrtime.bigint();
5 | const duration = (endTime - startTime) / BigInt(1e6);
6 | const timestamp = new Date().toISOString();
7 | const ip = req.socket.remoteAddress;
8 | const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`;
9 | return `${ip} - ${timestamp} ${httpDetails} - ${duration}ms`;
10 | }
11 |
12 | export { generateLogString };
13 |
--------------------------------------------------------------------------------
/07-eloop/isprime-child.mjs:
--------------------------------------------------------------------------------
1 | import { isPrime } from "./cpu-intensive.mjs";
2 |
3 | process.on("message", (msg) => {
4 | const result = isPrime(msg);
5 | process.send(result);
6 | process.exit();
7 | });
8 |
--------------------------------------------------------------------------------
/07-eloop/isprime-worker.mjs:
--------------------------------------------------------------------------------
1 | import { isPrime } from "./cpu-intensive.mjs";
2 | import { parentPort } from "worker_threads";
3 |
4 | parentPort.on("message", (msg) => {
5 | const result = isPrime(msg);
6 | parentPort.postMessage(result);
7 | });
8 |
--------------------------------------------------------------------------------
/07-eloop/isprime.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | int main(int argc, char **argv)
5 | {
6 | long i, num;
7 | if (argc < 2)
8 | {
9 | return 1;
10 | }
11 |
12 | num = strtol(argv[1], NULL, 10);
13 | if (num < 2)
14 | {
15 | return 1;
16 | }
17 | for (i = 2; i < num; ++i)
18 | {
19 | if (num % i == 0)
20 | {
21 | return 1;
22 | }
23 | }
24 | return 0;
25 | }
--------------------------------------------------------------------------------
/07-eloop/isprime.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { isPrime } from "./cpu-intensive.mjs";
3 |
4 | const num = parseInt(process.argv[2]);
5 | if (isNaN(num)) {
6 | console.error(`${num} is not a number`);
7 | process.exit(2);
8 | }
9 |
10 | if (isPrime(num)) {
11 | console.log(`${num} is prime`);
12 | process.exit(0);
13 | } else {
14 | console.log(`${num} is not prime`);
15 | process.exit(1);
16 | }
17 |
--------------------------------------------------------------------------------
/07-eloop/number.txt:
--------------------------------------------------------------------------------
1 | 39916801
--------------------------------------------------------------------------------
/08-pkg/01-check-strings.js:
--------------------------------------------------------------------------------
1 | const { isString } = require("./modules/is-string");
2 |
3 | const toCheck = [
4 | "a string",
5 | 2,
6 | { prop: "I'm an object" },
7 | function () {},
8 | true,
9 | ];
10 |
11 | for (const str of toCheck) {
12 | console.log(isString(str));
13 | }
14 |
--------------------------------------------------------------------------------
/08-pkg/02-manipulate-strings.js:
--------------------------------------------------------------------------------
1 | const { urlify } = require("./modules/string-utils");
2 | const { inspect } = require("util");
3 |
4 | const songs = [
5 | "Guerrilla Radio",
6 | "Killing in the name",
7 | "Bullet in the head",
8 | null,
9 | ];
10 |
11 | for (const s of songs) {
12 | try {
13 | console.log(`${s} -> ${urlify(s)}`);
14 | } catch (e) {
15 | console.error(`Skipped non string value: ${inspect(s)}`);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/08-pkg/03-debugging-time.js:
--------------------------------------------------------------------------------
1 | const debug = require("./modules/debug")("03");
2 |
3 | debug(`Script started at ${new Date().toISOString()}`);
4 | setTimeout(() => {
5 | // doing something...
6 |
7 | debug(`Script ended at ${new Date().toISOString()}`);
8 | }, 1000);
9 |
--------------------------------------------------------------------------------
/08-pkg/04-open-rad-map.js:
--------------------------------------------------------------------------------
1 | const debug = require("./modules/debug")("04");
2 | const open = require("./modules/open");
3 |
4 | debug("Script started");
5 | open("https://jciv.iidj.net/map/");
6 | debug("Function open called");
7 |
--------------------------------------------------------------------------------
/08-pkg/05-missing-mod.js:
--------------------------------------------------------------------------------
1 | require("./modules/my-mod");
2 |
--------------------------------------------------------------------------------
/08-pkg/06-shared-counter.js:
--------------------------------------------------------------------------------
1 | const counter1 = require("./modules/shared-counter");
2 | const counter2 = require("./modules/shared-counter");
3 |
4 | counter1.inc();
5 | counter2.inc();
6 |
7 | console.log(counter1.val());
8 |
--------------------------------------------------------------------------------
/08-pkg/07-lines-counter.mjs:
--------------------------------------------------------------------------------
1 | import { readFileLines, encoding } from "./modules/read-file-lines.mjs";
2 |
3 | const file = process.argv[2];
4 | const lines = await readFileLines(file);
5 | console.log(`${file} (${encoding}) is ${lines.length} lines long`);
6 |
--------------------------------------------------------------------------------
/08-pkg/08-numbered-echo.mjs:
--------------------------------------------------------------------------------
1 | import { readFileLines } from "./modules/read-file-lines.mjs";
2 | import printLines from "./modules/print-lines-numbers.mjs";
3 |
4 | const file = process.argv[2];
5 | const lines = await readFileLines(file);
6 | printLines(lines);
7 |
--------------------------------------------------------------------------------
/08-pkg/09-open-links.mjs:
--------------------------------------------------------------------------------
1 | import printLines from "./modules/print-lines-numbers.mjs";
2 | import { readFileLines as readLinks } from "./modules/read-file-lines.mjs";
3 | import readNumber from "./modules/read-number.mjs";
4 | import open from "./modules/open/open.js";
5 |
6 | const linksFile = "./links.txt";
7 | const countFrom = 1;
8 |
9 | const links = await readLinks(linksFile);
10 | printLines(links, countFrom);
11 | const num = await readNumber("Digit a number and press Enter to open it: ");
12 | open(links[num - countFrom]);
13 |
--------------------------------------------------------------------------------
/08-pkg/10-feed-reader/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es2022": true
5 | },
6 | "parserOptions": {
7 | "sourceType": "module"
8 | },
9 | "extends": "eslint:recommended"
10 | }
11 |
--------------------------------------------------------------------------------
/08-pkg/10-feed-reader/.gitignore:
--------------------------------------------------------------------------------
1 | feed-db.txt
2 | node_modules/
--------------------------------------------------------------------------------
/08-pkg/10-feed-reader/app.mjs:
--------------------------------------------------------------------------------
1 | import printLines from "../modules/print-lines-numbers.mjs";
2 | import readNumber from "../modules/read-number.mjs";
3 | import open from "../modules/open/open.js";
4 | import getFeedEntries from "./get-feeds.mjs";
5 |
6 | const countFrom = 1;
7 | const feedUrl = process.argv[2];
8 | const entries = await getFeedEntries(feedUrl);
9 | const titles = entries.map((e) => e.title);
10 |
11 | printLines(titles, countFrom);
12 | const num = await readNumber("Digit a number and press Enter to open it: ");
13 | const selectedEntry = entries[num - countFrom];
14 | open(selectedEntry.link);
15 |
--------------------------------------------------------------------------------
/08-pkg/10-feed-reader/get-feeds.mjs:
--------------------------------------------------------------------------------
1 | import { parseFeed } from "htmlparser2";
2 |
3 | export default async function getFeedEntries(url, maxEntries = 5) {
4 | const feedFetched = await fetch(url);
5 | if (!feedFetched.ok) {
6 | throw new Error("Feed fetch failed");
7 | }
8 | const feedContent = await feedFetched.text();
9 | const { items } = parseFeed(feedContent);
10 | return items.slice(0, maxEntries);
11 | }
12 |
--------------------------------------------------------------------------------
/08-pkg/10-feed-reader/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "10-feed-reader",
3 | "scripts": {
4 | "start": "node app.mjs",
5 | "node-blog": "node app.mjs https://nodejs.org/en/feed/blog.xml",
6 | "start-debug": "NODE_DEBUG=* node app.mjs"
7 | },
8 | "dependencies": {
9 | "htmlparser2": "^9.1.0"
10 | },
11 | "devDependencies": {
12 | "eslint": "^8.56.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/08-pkg/10-feed-reader/wrong.mjs:
--------------------------------------------------------------------------------
1 | import wrong from "i-am-not-here";
2 |
--------------------------------------------------------------------------------
/08-pkg/links.txt:
--------------------------------------------------------------------------------
1 | https://jciv.iidj.net/map/
2 | https://remap.jrc.ec.europa.eu/
3 | https://vimeo.com/135580602
4 | https://nuclearsecrecy.com/nukemap/
--------------------------------------------------------------------------------
/08-pkg/modules/bookmark-utils.mjs:
--------------------------------------------------------------------------------
1 | //TODO - unire tutto e fare tre export
2 |
--------------------------------------------------------------------------------
/08-pkg/modules/debug/index.js:
--------------------------------------------------------------------------------
1 | function debugFactory(tag) {
2 | const enabled = process.env.DEBUG;
3 |
4 | function debug(msg) {
5 | if (!enabled) {
6 | return;
7 | }
8 | console.log(`[${tag}] ${msg}`);
9 | }
10 | return debug;
11 | }
12 | module.exports = debugFactory;
13 |
--------------------------------------------------------------------------------
/08-pkg/modules/is-string.js:
--------------------------------------------------------------------------------
1 | function isString(str) {
2 | return typeof str === "string";
3 | }
4 |
5 | exports.isString = isString;
6 |
--------------------------------------------------------------------------------
/08-pkg/modules/open/open.js:
--------------------------------------------------------------------------------
1 | const process = require("process");
2 | const childProcess = require("child_process");
3 | const { isString } = require("../is-string");
4 | const debug = require("../debug")("open");
5 |
6 | const { platform } = process;
7 |
8 | const platformCmds = {
9 | darwin: "open",
10 | linux: "xdg-open",
11 | win32: "powershell.exe /c start",
12 | };
13 |
14 | debug("platform is " + platform);
15 |
16 | function open(target) {
17 | if (!isString(target)) {
18 | throw new TypeError("A target is required");
19 | }
20 | debug("target resource is " + target);
21 |
22 | const command = platformCmds[platform];
23 | if (!command) {
24 | throw new Error(`${platform} is not supported.`);
25 | }
26 |
27 | const childProcessOptions = {
28 | stdio: "ignore",
29 | detached: true,
30 | };
31 |
32 | const spawnedProc = childProcess.spawn(
33 | command,
34 | [target],
35 | childProcessOptions
36 | );
37 |
38 | spawnedProc.unref();
39 | debug(`Spawned process PID is ${spawnedProc.pid}`);
40 | return spawnedProc;
41 | }
42 |
43 | module.exports = open;
44 |
--------------------------------------------------------------------------------
/08-pkg/modules/open/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "./open.js"
3 | }
4 |
--------------------------------------------------------------------------------
/08-pkg/modules/print-lines-numbers.mjs:
--------------------------------------------------------------------------------
1 | function print(lines = [], countFrom = 0) {
2 | for (let idx = 0; idx < lines.length; idx++) {
3 | const line = lines[idx];
4 | console.log(`[${countFrom + idx}] ${line}`);
5 | }
6 | }
7 | export default print;
8 |
--------------------------------------------------------------------------------
/08-pkg/modules/read-file-lines.mjs:
--------------------------------------------------------------------------------
1 | import { readFile } from "node:fs/promises";
2 | const encoding = "utf-8";
3 | const separator = "\n";
4 |
5 | async function readFileLines(filePath, sep = separator, enc = encoding) {
6 | const fileContent = await readFile(filePath, { encoding: enc });
7 | const lines = fileContent.split(sep);
8 | return lines;
9 | }
10 |
11 | export { readFileLines, separator, encoding };
12 | export default readFileLines;
13 |
--------------------------------------------------------------------------------
/08-pkg/modules/read-number.mjs:
--------------------------------------------------------------------------------
1 | import * as readline from "node:readline/promises";
2 | import { stdin as input, stdout as output } from "node:process";
3 |
4 | export default async function (prompt = "Insert a number") {
5 | const rl = readline.createInterface({ input, output });
6 | const answer = await rl.question(prompt);
7 | rl.close();
8 | return parseInt(answer);
9 | }
10 |
--------------------------------------------------------------------------------
/08-pkg/modules/shared-counter/counter.js:
--------------------------------------------------------------------------------
1 | let counter = 0;
2 |
3 | exports.inc = () => ++counter;
4 | exports.dec = () => --counter;
5 | exports.val = () => counter;
6 |
--------------------------------------------------------------------------------
/08-pkg/modules/shared-counter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "./counter.js"
3 | }
4 |
--------------------------------------------------------------------------------
/08-pkg/modules/string-utils/index.js:
--------------------------------------------------------------------------------
1 | function isString(str) {
2 | return typeof str === "string";
3 | }
4 |
5 | function toUrl(str) {
6 | if (!isString(str)) {
7 | throw new TypeError("A string is required");
8 | }
9 | return str.toLowerCase().replace(/[^a-z0-9]+/g, "-");
10 | }
11 |
12 | exports.isString = isString;
13 | exports.urlify = toUrl;
14 |
--------------------------------------------------------------------------------
/09-express/01-apod/apod-api.mjs:
--------------------------------------------------------------------------------
1 | const API_URL = "https://api.nasa.gov/planetary/apod";
2 | const API_KEY = process.env.NASA_API_KEY || "DEMO_KEY";
3 |
4 | export async function getApodByDate(dateStr) {
5 | const res = await fetch(`${API_URL}?date=${dateStr}&api_key=${API_KEY}`);
6 | if (res.ok) {
7 | const json = await res.json();
8 | return json;
9 | } else {
10 | throw new Error(`API return a ${res.status} code`);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v1.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const app = express();
3 | const port = 3000;
4 |
5 | app.get("/", (req, res) => {
6 | res.send("Hello APOD!");
7 | });
8 | app.get("/about", (req, res) => {
9 | res.send(`
10 |
11 |
12 | About APOD
13 |
14 | Astronomy Picture of the Day
15 |
16 | Astronomy Picture of the Day (APOD) is originated, written, coordinated,
17 | and edited since 1995 by Robert Nemiroff and Jerry Bonnell.
18 | The APOD archive contains the largest collection of annotated
19 | astronomical images on the internet.
20 |
21 |
22 | `);
23 | });
24 |
25 | app.listen(port, () => {
26 | console.log(`APOD app listening on port ${port}`);
27 | });
28 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v2.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const app = express();
3 | const port = 3000;
4 |
5 | app.use(express.static("public"));
6 |
7 | app.get("/", (req, res) => {
8 | res.send(`
9 |
10 |
11 |
12 | APOD
13 |
14 |
15 |
16 |
17 | Astronomy Picture of the Day
18 |
19 |
20 |
21 |
22 | `);
23 | });
24 | app.get("/about", (req, res) => {
25 | res.send(`
26 |
27 |
28 | About APOD
29 |
30 | Astronomy Picture of the Day
31 |
32 | Astronomy Picture of the Day (APOD) is originated, written, coordinated,
33 | and edited since 1995 by Robert Nemiroff and Jerry Bonnell.
34 | The APOD archive contains the largest collection of annotated
35 | astronomical images on the internet.
36 |
37 |
38 | `);
39 | });
40 |
41 | app.listen(port, () => {
42 | console.log(`APOD app listening on port ${port}`);
43 | });
44 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v3.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const app = express();
3 | const port = 3000;
4 |
5 | app.set("view engine", "hbs");
6 |
7 | app.use(express.static("public"));
8 |
9 | app.get("/", (req, res) => {
10 | res.render("home", {
11 | title: "Astronomy Picture of the Day",
12 | pic_url: "/saturn-voyager.jpg",
13 | });
14 | });
15 | app.get("/about", (req, res) => {
16 | res.render("about");
17 | });
18 |
19 | app.listen(port, () => {
20 | console.log(`APOD app listening on port ${port}`);
21 | });
22 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v4.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const app = express();
3 | const port = 3000;
4 |
5 | app.set("views", "./views2");
6 | app.set("view engine", "hbs");
7 |
8 | app.use(express.static("public"));
9 |
10 | app.get("/", (req, res) => {
11 | res.render("home", {
12 | title: "Astronomy Picture of the Day",
13 | pic_url: "/saturn-voyager.jpg",
14 | });
15 | });
16 | app.get("/about", (req, res) => {
17 | res.render("about", {
18 | title: "About APOD",
19 | });
20 | });
21 |
22 | app.listen(port, () => {
23 | console.log(`APOD app listening on port ${port}`);
24 | });
25 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v5.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const app = express();
3 | const port = 3000;
4 |
5 | app.set("views", "./views2");
6 | app.set("view engine", "hbs");
7 |
8 | app.use(express.static("public"));
9 |
10 | app.get("/", (req, res) => {
11 | res.render("home", {
12 | title: "Astronomy Picture of the Day",
13 | pic_url: "/saturn-voyager.jpg",
14 | });
15 | });
16 | app.get("/apod/:date", (req, res) => {
17 | res.render("apod-pic", {
18 | title: "Astronomy Picture of the Day",
19 | date: req.params.date,
20 | });
21 | });
22 | app.get("/about", (req, res) => {
23 | res.render("about", {
24 | title: "About APOD",
25 | });
26 | });
27 |
28 | app.listen(port, () => {
29 | console.log(`APOD app listening on port ${port}`);
30 | });
31 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v6.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { parse, isBefore, isFuture, isValid } from "date-fns";
3 |
4 | const app = express();
5 | const port = 3000;
6 |
7 | app.set("views", "./views2");
8 | app.set("view engine", "hbs");
9 |
10 | app.use(express.static("public"));
11 |
12 | app.get("/", (req, res) => {
13 | res.render("home", {
14 | title: "Astronomy Picture of the Day",
15 | pic_url: "/saturn-voyager.jpg",
16 | });
17 | });
18 |
19 | const firstApodDate = new Date("1995-06-16");
20 | function validateDate(req, res, next) {
21 | const date = parse(req.params.date, "yyyy-MM-dd", new Date());
22 | if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) {
23 | res.status(404);
24 | res.render("apod-not-found");
25 | } else {
26 | next();
27 | }
28 | }
29 | app.get("/apod/:date", validateDate, (req, res) => {
30 | res.render("apod-pic", {
31 | title: "Astronomy Picture of the Day",
32 | date: req.params.date,
33 | });
34 | });
35 |
36 | app.get("/about", (req, res) => {
37 | res.render("about", {
38 | title: "About APOD",
39 | });
40 | });
41 |
42 | app.listen(port, () => {
43 | console.log(`APOD app listening on port ${port}`);
44 | });
45 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v7.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { parse, isBefore, isFuture, isValid, format } from "date-fns";
3 | import { getApodByDate } from "./apod-api.mjs";
4 |
5 | const app = express();
6 | const port = 3000;
7 |
8 | app.set("views", "./views2");
9 | app.set("view engine", "hbs");
10 |
11 | app.use(express.static("public"));
12 |
13 | app.get("/", (req, res) => {
14 | const today = format(new Date(), "yyyy-MM-dd");
15 | getApodByDate(today).then((apodInfo) => {
16 | res.render("apod", {
17 | title: `${apodInfo.title} | Astronomy Picture of the Day`,
18 | date: today,
19 | url: apodInfo.url,
20 | description: apodInfo.explanation,
21 | });
22 | });
23 | });
24 |
25 | const firstApodDate = new Date("1995-06-16");
26 | function validateDate(req, res, next) {
27 | const date = parse(req.params.date, "yyyy-MM-dd", new Date());
28 | if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) {
29 | res.status(404);
30 | res.render("apod-not-found");
31 | } else {
32 | next();
33 | }
34 | }
35 | app.get("/apod/:date", validateDate, (req, res) => {
36 | getApodByDate(req.params.date).then((apodInfo) => {
37 | res.render("apod", {
38 | title: `${apodInfo.title} | Astronomy Picture of the Day`,
39 | date: req.params.date,
40 | url: apodInfo.url,
41 | description: apodInfo.explanation,
42 | });
43 | });
44 | });
45 |
46 | app.get("/about", (req, res) => {
47 | res.render("about", {
48 | title: "About APOD",
49 | });
50 | });
51 |
52 | app.listen(port, () => {
53 | console.log(`APOD app listening on port ${port}`);
54 | });
55 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v8.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { parse, isBefore, isFuture, isValid, format } from "date-fns";
3 | import { getApodByDate } from "./apod-api.mjs";
4 |
5 | const app = express();
6 | const port = 3000;
7 |
8 | app.set("views", "./views2");
9 | app.set("view engine", "hbs");
10 |
11 | app.use(express.static("public"));
12 |
13 | app.get("/", (req, res) => {
14 | const today = format(new Date(), "yyyy-MM-dd");
15 | getApodByDate(today).then((apodInfo) => {
16 | res.render("apod-full", {
17 | title: `${apodInfo.title} | Astronomy Picture of the Day`,
18 | date: today,
19 | isImage: apodInfo.media_type === "image",
20 | url: apodInfo.url,
21 | description: apodInfo.explanation,
22 | });
23 | });
24 | });
25 |
26 | const firstApodDate = new Date("1995-06-16");
27 | function validateDate(req, res, next) {
28 | const date = parse(req.params.date, "yyyy-MM-dd", new Date());
29 | if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) {
30 | res.status(404);
31 | res.render("apod-not-found");
32 | } else {
33 | next();
34 | }
35 | }
36 | app.get("/apod/:date", validateDate, (req, res) => {
37 | getApodByDate(req.params.date).then((apodInfo) => {
38 | res.render("apod-full", {
39 | title: `${apodInfo.title} | Astronomy Picture of the Day`,
40 | date: req.params.date,
41 | isImage: apodInfo.media_type === "image",
42 | url: apodInfo.url,
43 | description: apodInfo.explanation,
44 | });
45 | });
46 | });
47 |
48 | app.get("/about", (req, res) => {
49 | res.render("about", {
50 | title: "About APOD",
51 | });
52 | });
53 |
54 | app.listen(port, () => {
55 | console.log(`APOD app listening on port ${port}`);
56 | });
57 |
--------------------------------------------------------------------------------
/09-express/01-apod/app_v9.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { parse, isBefore, isFuture, isValid, format } from "date-fns";
3 | import { getApodByDate } from "./apod-api.mjs";
4 |
5 | const app = express();
6 | const port = 3000;
7 |
8 | app.set("views", "./views2");
9 | app.set("view engine", "hbs");
10 |
11 | app.use(express.static("public"));
12 |
13 | app.get("/", (req, res, next) => {
14 | const today = format(new Date(), "yyyy-MM-dd");
15 | getApodByDate(today)
16 | .then((apodInfo) => {
17 | res.render("apod-full", {
18 | title: `${apodInfo.title} | Astronomy Picture of the Day`,
19 | date: today,
20 | isImage: apodInfo.media_type === "image",
21 | url: apodInfo.url,
22 | description: apodInfo.explanation,
23 | });
24 | })
25 | .catch(next);
26 | });
27 |
28 | const firstApodDate = new Date("1995-06-16");
29 | function validateDate(req, res, next) {
30 | const date = parse(req.params.date, "yyyy-MM-dd", new Date());
31 | if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) {
32 | res.status(404);
33 | res.render("apod-not-found");
34 | } else {
35 | next();
36 | }
37 | }
38 | app.get("/apod/:date", validateDate, (req, res, next) => {
39 | getApodByDate(req.params.date)
40 | .then((apodInfo) => {
41 | res.render("apod-full", {
42 | title: `${apodInfo.title} | Astronomy Picture of the Day`,
43 | date: req.params.date,
44 | isImage: apodInfo.media_type === "image",
45 | url: apodInfo.url,
46 | description: apodInfo.explanation,
47 | });
48 | })
49 | .catch(next);
50 | });
51 |
52 | app.get("/about", (req, res) => {
53 | res.render("about", {
54 | title: "About APOD",
55 | });
56 | });
57 |
58 | app.use((req, res, next) => {
59 | res.status(404);
60 | res.render("404");
61 | });
62 |
63 | app.use((err, req, res, next) => {
64 | if (res.headersSent) {
65 | return next(err);
66 | }
67 | res.status(500);
68 | res.render("500");
69 | });
70 |
71 | app.listen(port, () => {
72 | console.log(`APOD app listening on port ${port}`);
73 | });
74 |
--------------------------------------------------------------------------------
/09-express/01-apod/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "01-apod",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "date-fns": "^3.3.1",
14 | "express": "^4.18.2",
15 | "hbs": "^4.2.0"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/09-express/01-apod/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lifeisfoo/node-js/5d28f338f32b0035ad8426939c2c92c4d936f8ff/09-express/01-apod/public/favicon.ico
--------------------------------------------------------------------------------
/09-express/01-apod/public/saturn-voyager.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lifeisfoo/node-js/5d28f338f32b0035ad8426939c2c92c4d936f8ff/09-express/01-apod/public/saturn-voyager.jpg
--------------------------------------------------------------------------------
/09-express/01-apod/views/about.hbs:
--------------------------------------------------------------------------------
1 |
2 | About APOD
3 |
4 | Astronomy Picture of the Day
5 |
6 | Astronomy Picture of the Day (APOD) is originated, written,
7 | coordinated, and edited since 1995 by Robert Nemiroff and Jerry
8 | Bonnell. The APOD archive contains the largest collection of
9 | annotated astronomical images on the internet.
10 |
11 |
12 |
--------------------------------------------------------------------------------
/09-express/01-apod/views/home.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{title}}
4 |
5 |
6 |
7 |
8 | {{title}}
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/09-express/01-apod/views2/404.hbs:
--------------------------------------------------------------------------------
1 | 404
2 | page not found :\
--------------------------------------------------------------------------------
/09-express/01-apod/views2/500.hbs:
--------------------------------------------------------------------------------
1 | Internal server error :\
--------------------------------------------------------------------------------
/09-express/01-apod/views2/about.hbs:
--------------------------------------------------------------------------------
1 | {{title}}
2 |
3 | Astronomy Picture of the Day (APOD) is originated, written, coordinated, and
4 | edited since 1995 by Robert Nemiroff and Jerry Bonnell. The APOD archive
5 | contains the largest collection of annotated astronomical images on the
6 | internet.
7 |
--------------------------------------------------------------------------------
/09-express/01-apod/views2/apod-full.hbs:
--------------------------------------------------------------------------------
1 | {{title}}
2 | Picture of {{date}}
3 | {{#if isImage}}
4 |
5 | {{else}}
6 | Open video
7 | {{/if}}
8 | {{description}}
--------------------------------------------------------------------------------
/09-express/01-apod/views2/apod-not-found.hbs:
--------------------------------------------------------------------------------
1 | APOD not found
2 | Please, try with another date
--------------------------------------------------------------------------------
/09-express/01-apod/views2/apod-pic.hbs:
--------------------------------------------------------------------------------
1 | {{title}}
2 | Picture of {{date}}
--------------------------------------------------------------------------------
/09-express/01-apod/views2/apod.hbs:
--------------------------------------------------------------------------------
1 | {{title}}
2 | Picture of {{date}}
3 |
4 | {{description}}
--------------------------------------------------------------------------------
/09-express/01-apod/views2/home.hbs:
--------------------------------------------------------------------------------
1 | {{title}}
2 |
--------------------------------------------------------------------------------
/09-express/01-apod/views2/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{title}}
7 |
8 |
9 |
18 |
19 | {{{body}}}
20 |
21 |
29 |
30 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v1.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getLatestEntries } from "./get-feeds.mjs";
3 | const app = express();
4 | const port = 3000;
5 |
6 | const wrap =
7 | (fn) =>
8 | (...args) =>
9 | fn(...args).catch(args[2]);
10 |
11 | const sources = [];
12 |
13 | app.use(express.json());
14 |
15 | app.post("/sources", (req, res) => {
16 | sources.push(req.body.url);
17 | res.json(sources);
18 | });
19 |
20 | app.get("/sources", (req, res) => {
21 | res.json(sources);
22 | });
23 |
24 | app.get(
25 | "/feeds",
26 | wrap(async (req, res) => {
27 | const entries = await getLatestEntries(sources);
28 | res.json(entries);
29 | })
30 | );
31 |
32 | app.listen(port, () => {
33 | console.log(`FEED app listening on port ${port}`);
34 | });
35 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v2.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getLatestEntries } from "./get-feeds.mjs";
3 | import { hrtime } from "process";
4 | import compression from "compression";
5 |
6 | const app = express();
7 | const port = 3000;
8 |
9 | const wrap =
10 | (fn) =>
11 | (...args) =>
12 | fn(...args).catch(args[2]);
13 |
14 | const sources = [];
15 |
16 | app.use((req, res, next) => {
17 | res.startTime = hrtime.bigint();
18 |
19 | res.on("finish", () => {
20 | const duration = (hrtime.bigint() - res.startTime) / BigInt(1e6);
21 | console.log(
22 | `${req.method} ${req.path} ${res.statusCode} - ${duration}ms`
23 | );
24 | });
25 | next();
26 | });
27 |
28 | app.use(compression());
29 | app.use(express.json());
30 |
31 | app.post("/sources", (req, res) => {
32 | sources.push(req.body.url);
33 | res.json(sources);
34 | });
35 |
36 | app.get("/sources", (req, res) => {
37 | res.json(sources);
38 | });
39 |
40 | app.get(
41 | "/feeds",
42 | wrap(async (req, res) => {
43 | const entries = await getLatestEntries(sources);
44 | res.json(entries);
45 | })
46 | );
47 |
48 | app.listen(port, () => {
49 | console.log(`FEED app listening on port ${port}`);
50 | });
51 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v3.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getLatestEntries } from "./get-feeds.mjs";
3 | import compression from "compression";
4 | import pino from "pino-http";
5 |
6 | const app = express();
7 | const port = 3000;
8 |
9 | const wrap =
10 | (fn) =>
11 | (...args) =>
12 | fn(...args).catch(args[2]);
13 |
14 | const sources = [];
15 |
16 | app.use(pino());
17 | app.use(compression());
18 | app.use(express.json());
19 |
20 | app.post("/sources", (req, res) => {
21 | sources.push(req.body.url);
22 | req.log.info(`New URL added to sources: ${req.body.url}`);
23 | res.json(sources);
24 | });
25 |
26 | app.get("/sources", (req, res) => {
27 | res.json(sources);
28 | });
29 |
30 | app.get(
31 | "/feeds",
32 | wrap(async (req, res) => {
33 | req.log.debug(`Feed sources are ${sources}`);
34 | const entries = await getLatestEntries(sources);
35 | res.json(entries);
36 | })
37 | );
38 |
39 | app.listen(port, () => {
40 | console.log(`FEED app listening on port ${port}`);
41 | });
42 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v4.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getLatestEntries } from "./get-feeds.mjs";
3 | import compression from "compression";
4 | import pino from "pino-http";
5 | import mongoose from "mongoose";
6 | import { Source } from "./models/Source.mjs";
7 |
8 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
9 |
10 | const app = express();
11 | const port = 3000;
12 |
13 | const wrap =
14 | (fn) =>
15 | (...args) =>
16 | fn(...args).catch(args[2]);
17 |
18 | app.use(pino());
19 | app.use(compression());
20 | app.use(express.json());
21 |
22 | app.post(
23 | "/sources",
24 | wrap(async (req, res) => {
25 | const source = new Source({ url: req.body.url });
26 | req.log.info(`Adding new Source: ${source}`);
27 | await source.save();
28 | res.json(source);
29 | })
30 | );
31 |
32 | app.get(
33 | "/sources",
34 | wrap(async (req, res) => {
35 | const sources = await Source.find({}).exec();
36 | res.json(sources);
37 | })
38 | );
39 |
40 | app.get(
41 | "/feeds",
42 | wrap(async (req, res) => {
43 | const sources = await Source.find({}).exec();
44 | const urls = sources.map((s) => s.url);
45 | req.log.debug(`Feed sources are ${urls}`);
46 | const entries = await getLatestEntries(urls);
47 | res.json(entries);
48 | })
49 | );
50 |
51 | app.listen(port, () => {
52 | console.log(`FEED app listening on port ${port}`);
53 | });
54 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v5.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getLatestEntries } from "./get-feeds.mjs";
3 | import compression from "compression";
4 | import pino from "pino-http";
5 | import mongoose from "mongoose";
6 | import { Source } from "./models/Source_v1.mjs";
7 |
8 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
9 |
10 | const app = express();
11 | const port = 3000;
12 |
13 | const wrap =
14 | (fn) =>
15 | (...args) =>
16 | fn(...args).catch(args[2]);
17 |
18 | app.use(pino());
19 | app.use(compression());
20 | app.use(express.json());
21 |
22 | app.post(
23 | "/sources",
24 | wrap(async (req, res) => {
25 | const source = new Source({ url: req.body.url });
26 | req.log.info(`Adding new Source: ${source}`);
27 | try {
28 | await source.save();
29 | res.json(source);
30 | } catch (e) {
31 | req.log.error("An error occurred during Source.save()");
32 | res.status(500).send();
33 | }
34 | })
35 | );
36 |
37 | app.get(
38 | "/sources",
39 | wrap(async (req, res) => {
40 | const sources = await Source.find({}).exec();
41 | res.json(sources);
42 | })
43 | );
44 |
45 | app.delete(
46 | "/sources/:id",
47 | wrap(async (req, res) => {
48 | try {
49 | await Source.findByIdAndDelete(req.params.id).exec();
50 | res.status(200).send();
51 | } catch {
52 | req.log.error(
53 | `Error while deleting Source with id ${req.params.id}`
54 | );
55 | }
56 | })
57 | );
58 |
59 | app.get(
60 | "/feeds",
61 | wrap(async (req, res) => {
62 | const sources = await Source.find({}).exec();
63 | const urls = sources.map((s) => s.url);
64 | req.log.debug(`Feed sources are ${urls}`);
65 | const entries = await getLatestEntries(urls);
66 | res.json(entries);
67 | })
68 | );
69 |
70 | app.listen(port, () => {
71 | console.log(`FEED app listening on port ${port}`);
72 | });
73 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v6.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import compression from "compression";
3 | import pino from "pino-http";
4 | import mongoose from "mongoose";
5 | import { Source } from "./models/Source_v1.mjs";
6 |
7 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
8 |
9 | const app = express();
10 | const port = 3000;
11 |
12 | const wrap =
13 | (fn) =>
14 | (...args) =>
15 | fn(...args).catch(args[2]);
16 |
17 | app.use(pino());
18 | app.use(compression());
19 | app.use(express.json());
20 |
21 | app.post(
22 | "/sources",
23 | wrap(async (req, res) => {
24 | const source = new Source({ url: req.body.url });
25 | req.log.info(`Adding new Source: ${source}`);
26 | try {
27 | await source.save();
28 | res.json(source);
29 | } catch (e) {
30 | const reason = Source.getErrorReason(e);
31 | if (reason) {
32 | req.log.error(reason);
33 | res.status(400).send();
34 | } else {
35 | req.log.error("An error occurred during Source.save()");
36 | res.status(500).send();
37 | }
38 | }
39 | })
40 | );
41 |
42 | app.get(
43 | "/sources",
44 | wrap(async (req, res) => {
45 | const sources = await Source.find({}).exec();
46 | res.json(sources);
47 | })
48 | );
49 |
50 | app.delete(
51 | "/sources/:id",
52 | wrap(async (req, res) => {
53 | try {
54 | await Source.findByIdAndDelete(req.params.id).exec();
55 | res.status(200).send();
56 | } catch {
57 | req.log.error(
58 | `Error while deleting Source with id ${req.params.id}`
59 | );
60 | }
61 | })
62 | );
63 |
64 | app.get(
65 | "/feeds",
66 | wrap(async (req, res) => {
67 | const sources = await Source.find({}).exec();
68 | const urls = sources.map((s) => s.url);
69 | req.log.debug(`Feed sources are ${urls}`);
70 | const entries = await getLatestEntries(urls);
71 | res.json(entries);
72 | })
73 | );
74 |
75 | app.listen(port, () => {
76 | console.log(`FEED app listening on port ${port}`);
77 | });
78 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v7.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import getFeedEntries from "./get-feeds.mjs";
3 | import compression from "compression";
4 | import pino from "pino-http";
5 | import mongoose from "mongoose";
6 | import { Source } from "./models/Source_v1.mjs";
7 | import { FeedEntry } from "./models/FeedEntry.mjs";
8 |
9 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
10 |
11 | const app = express();
12 | const port = 3000;
13 |
14 | const wrap =
15 | (fn) =>
16 | (...args) =>
17 | fn(...args).catch(args[2]);
18 |
19 | app.use(pino());
20 | app.use(compression());
21 | app.use(express.json());
22 |
23 | app.post(
24 | "/sources",
25 | wrap(async (req, res) => {
26 | const source = new Source({ url: req.body.url });
27 | req.log.info(`Adding new Source: ${source}`);
28 | try {
29 | await source.save();
30 | res.json(source);
31 | } catch (e) {
32 | const reason = Source.getErrorReason(e);
33 | if (reason) {
34 | req.log.error(reason);
35 | res.status(400).send();
36 | } else {
37 | req.log.error("An error occurred during Source.save()");
38 | res.status(500).send();
39 | }
40 | }
41 | })
42 | );
43 |
44 | app.get(
45 | "/sources",
46 | wrap(async (req, res) => {
47 | const sources = await Source.find({}).exec();
48 | res.json(sources);
49 | })
50 | );
51 |
52 | app.delete(
53 | "/sources/:id",
54 | wrap(async (req, res) => {
55 | try {
56 | await Source.findByIdAndDelete(req.params.id).exec();
57 | res.status(200).send();
58 | } catch {
59 | req.log.error(
60 | `Error while deleting Source with id ${req.params.id}`
61 | );
62 | }
63 | })
64 | );
65 |
66 | app.get(
67 | "/feeds",
68 | wrap(async (req, res) => {
69 | let findQuery = {};
70 | if (req.query.sourceId) {
71 | findQuery.source = req.query.sourceId;
72 | }
73 | const feedEntries = await FeedEntry.find(findQuery)
74 | .sort({ date: -1 })
75 | .limit(20)
76 | .exec();
77 | res.json(feedEntries);
78 | })
79 | );
80 | function generateFeedEntries(entries, s) {
81 | return entries.map((en) => {
82 | return new FeedEntry({
83 | source: s._id,
84 | title: en.title,
85 | link: en.link,
86 | date: new Date(en.pubDate),
87 | }).save();
88 | });
89 | }
90 | async function waitAndCountFulfilled(proms) {
91 | const promsSettled = await Promise.allSettled(proms);
92 | return promsSettled.filter((r) => r.status === "fulfilled").length;
93 | }
94 | app.post(
95 | "/feeds-update",
96 | wrap(async (req, res) => {
97 | const sources = await Source.find({}).exec();
98 | let totalNewEntries = 0;
99 | for (const s of sources) {
100 | const entries = await getFeedEntries(s.url);
101 | const savingModels = generateFeedEntries(entries, s);
102 | let newCount = await waitAndCountFulfilled(savingModels);
103 | totalNewEntries += newCount;
104 | req.log.info(
105 | `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries`
106 | );
107 | }
108 | res.json({ totalNewEntries });
109 | })
110 | );
111 |
112 | app.listen(port, () => {
113 | console.log(`FEED app listening on port ${port}`);
114 | });
115 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/app_v8.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import getFeedEntries from "./get-feeds.mjs";
3 | import compression from "compression";
4 | import pino from "pino-http";
5 | import mongoose from "mongoose";
6 | import { Source } from "./models/Source_v1.mjs";
7 | import { FeedEntry } from "./models/FeedEntry.mjs";
8 |
9 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
10 |
11 | const app = express();
12 | const port = 3000;
13 |
14 | const wrap =
15 | (fn) =>
16 | (...args) =>
17 | fn(...args).catch(args[2]);
18 |
19 | app.use(pino());
20 | app.use(compression());
21 | app.use(express.json());
22 |
23 | app.post(
24 | "/sources",
25 | wrap(async (req, res) => {
26 | const source = new Source({ url: req.body.url });
27 | req.log.info(`Adding new Source: ${source}`);
28 | try {
29 | await source.save();
30 | res.json(source);
31 | } catch (e) {
32 | const reason = Source.getErrorReason(e);
33 | if (reason) {
34 | req.log.error(reason);
35 | res.status(400).send();
36 | } else {
37 | req.log.error("An error occurred during Source.save()");
38 | res.status(500).send();
39 | }
40 | }
41 | })
42 | );
43 |
44 | app.get(
45 | "/sources",
46 | wrap(async (req, res) => {
47 | const sources = await Source.find({}).exec();
48 | res.json(sources);
49 | })
50 | );
51 |
52 | app.delete(
53 | "/sources/:id",
54 | wrap(async (req, res) => {
55 | try {
56 | await Source.findByIdAndDelete(req.params.id).exec();
57 | res.status(200).send();
58 | } catch {
59 | req.log.error(
60 | `Error while deleting Source with id ${req.params.id}`
61 | );
62 | }
63 | })
64 | );
65 |
66 | app.get(
67 | "/feeds",
68 | wrap(async (req, res) => {
69 | let findQuery = {};
70 | if (req.query.sourceId) {
71 | findQuery.source = req.query.sourceId;
72 | }
73 | const feedEntries = await FeedEntry.find(findQuery)
74 | .sort({ date: -1 })
75 | .limit(20)
76 | .populate("source")
77 | .exec();
78 | res.json(feedEntries);
79 | })
80 | );
81 | function generateFeedEntries(entries, s) {
82 | return entries.map((en) => {
83 | return new FeedEntry({
84 | source: s._id,
85 | title: en.title,
86 | link: en.link,
87 | date: new Date(en.pubDate),
88 | }).save();
89 | });
90 | }
91 | async function waitAndCountFulfilled(proms) {
92 | const promsSettled = await Promise.allSettled(proms);
93 | return promsSettled.filter((r) => r.status === "fulfilled").length;
94 | }
95 | app.post(
96 | "/feeds-update",
97 | wrap(async (req, res) => {
98 | const sources = await Source.find({}).exec();
99 | let totalNewEntries = 0;
100 | for (const s of sources) {
101 | const entries = await getFeedEntries(s.url);
102 | const savingModels = generateFeedEntries(entries, s);
103 | let newCount = await waitAndCountFulfilled(savingModels);
104 | totalNewEntries += newCount;
105 | req.log.info(
106 | `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries`
107 | );
108 | }
109 | res.json({ totalNewEntries });
110 | })
111 | );
112 |
113 | app.listen(port, () => {
114 | console.log(`FEED app listening on port ${port}`);
115 | });
116 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/get-feeds.mjs:
--------------------------------------------------------------------------------
1 | import { parseFeed } from "htmlparser2";
2 | import { compareDesc } from "date-fns";
3 |
4 | export default async function getFeedEntries(url, maxEntries = 5) {
5 | const feedFetched = await fetch(url);
6 | if (!feedFetched.ok) {
7 | throw new Error("Feed fetch failed");
8 | }
9 | const feedContent = await feedFetched.text();
10 | const { items } = parseFeed(feedContent);
11 | return items.slice(0, maxEntries);
12 | }
13 |
14 | export async function getLatestEntries(urls) {
15 | const feedsFetches = urls.map((s) => getFeedEntries(s));
16 | const fetchesPromises = await Promise.allSettled(feedsFetches);
17 | const retrievedFeeds = fetchesPromises
18 | .filter((r) => r.status === "fulfilled")
19 | .map((r) => r.value);
20 | const latestItems = retrievedFeeds.flat().map((i) => {
21 | return { title: i.title, link: i.link, date: i.pubDate };
22 | });
23 | latestItems.sort((a, b) => compareDesc(a.date, b.date));
24 | return latestItems;
25 | }
26 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/models/FeedEntry.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const feedEntrySchema = new Schema({
5 | source: { type: Schema.Types.ObjectId, ref: "Source" },
6 | title: { type: String },
7 | link: { type: String, unique: true },
8 | date: { type: Date },
9 | });
10 |
11 | const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema);
12 | export { FeedEntry };
13 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/models/Source.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const sourceSchema = new Schema({
5 | url: { type: String, required: true },
6 | });
7 |
8 | const Source = mongoose.model("Source", sourceSchema);
9 | export { Source };
10 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/models/Source_v1.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const sourceSchema = new Schema({
5 | url: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | validate: {
10 | validator: function (v) {
11 | try {
12 | const url = new URL(v);
13 | if (!url.protocol.startsWith("http")) {
14 | return false;
15 | }
16 | } catch {
17 | return false;
18 | }
19 | return true;
20 | },
21 | message: (props) => `${props.value} is not a valid URL`,
22 | },
23 | },
24 | });
25 | sourceSchema.static("getErrorReason", (e) => {
26 | if (e.name === "ValidationError") {
27 | return "Source validation failed";
28 | } else if (e.code === 11000) {
29 | return "Feed already exists";
30 | } else {
31 | return null;
32 | }
33 | });
34 |
35 | const Source = mongoose.model("Source", sourceSchema);
36 | export { Source };
37 |
--------------------------------------------------------------------------------
/09-express/02-feed-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "02-feed-api",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "compression": "^1.7.4",
14 | "date-fns": "^3.3.1",
15 | "express": "^4.18.2",
16 | "htmlparser2": "^9.1.0",
17 | "mongoose": "^8.1.3",
18 | "pino-http": "^9.0.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/app.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import compression from "compression";
3 | import pino from "pino-http";
4 | import mongoose from "mongoose";
5 | import sourcesRoutes from "./routes/sources.mjs";
6 | import feedsRoutes from "./routes/feeds.mjs";
7 | import feedsUpdateRoutes from "./routes/feeds-update.mjs";
8 |
9 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
10 | const app = express();
11 | const port = 3000;
12 |
13 | app.use(pino());
14 | app.use(compression());
15 |
16 | app.use("/sources", sourcesRoutes);
17 | app.use("/feeds", feedsRoutes);
18 | app.use("/feeds-update", feedsUpdateRoutes);
19 |
20 | app.listen(port, () => {
21 | console.log(`FEED app listening on port ${port}`);
22 | });
23 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/get-feeds.mjs:
--------------------------------------------------------------------------------
1 | import { parseFeed } from "htmlparser2";
2 | import { compareDesc } from "date-fns";
3 |
4 | export default async function getFeedEntries(url, maxEntries = 5) {
5 | const feedFetched = await fetch(url);
6 | if (!feedFetched.ok) {
7 | throw new Error("Feed fetch failed");
8 | }
9 | const feedContent = await feedFetched.text();
10 | const { items } = parseFeed(feedContent);
11 | return items.slice(0, maxEntries);
12 | }
13 |
14 | export async function getLatestEntries(urls) {
15 | const feedsFetches = urls.map((s) => getFeedEntries(s));
16 | const fetchesPromises = await Promise.allSettled(feedsFetches);
17 | const retrievedFeeds = fetchesPromises
18 | .filter((r) => r.status === "fulfilled")
19 | .map((r) => r.value);
20 | const latestItems = retrievedFeeds.flat().map((i) => {
21 | return { title: i.title, link: i.link, date: i.pubDate };
22 | });
23 | latestItems.sort((a, b) => compareDesc(a.date, b.date));
24 | return latestItems;
25 | }
26 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/models/FeedEntry.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const feedEntrySchema = new Schema({
5 | source: { type: Schema.Types.ObjectId, ref: "Source" },
6 | title: { type: String },
7 | link: { type: String, unique: true },
8 | date: { type: Date },
9 | });
10 |
11 | const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema);
12 | export { FeedEntry };
13 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/models/Source.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const sourceSchema = new Schema({
5 | url: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | validate: {
10 | validator: function (v) {
11 | try {
12 | const url = new URL(v);
13 | if (!url.protocol.startsWith("http")) {
14 | return false;
15 | }
16 | } catch {
17 | return false;
18 | }
19 | return true;
20 | },
21 | message: (props) => `${props.value} is not a valid URL`,
22 | },
23 | },
24 | });
25 | sourceSchema.static("getErrorReason", (e) => {
26 | if (e.name === "ValidationError") {
27 | return "Source validation failed";
28 | } else if (e.code === 11000) {
29 | return "Feed already exists";
30 | } else {
31 | return null;
32 | }
33 | });
34 |
35 | const Source = mongoose.model("Source", sourceSchema);
36 | export { Source };
37 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "03-feed-api-router",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "compression": "^1.7.4",
14 | "date-fns": "^3.3.1",
15 | "express": "^4.18.2",
16 | "htmlparser2": "^9.1.0",
17 | "mongoose": "^8.1.3",
18 | "pino-http": "^9.0.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/routes/feeds-update.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { Source } from "../models/Source.mjs";
3 | import { wrap } from "../utils.mjs";
4 | import getFeedEntries from "../get-feeds.mjs";
5 | import { FeedEntry } from "../models/FeedEntry.mjs";
6 |
7 | const router = express.Router();
8 |
9 | function generateFeedEntries(entries, s) {
10 | return entries.map((en) => {
11 | return new FeedEntry({
12 | source: s._id,
13 | title: en.title,
14 | link: en.link,
15 | date: new Date(en.pubDate),
16 | }).save();
17 | });
18 | }
19 | async function waitAndCountFulfilled(proms) {
20 | const promsSettled = await Promise.allSettled(proms);
21 | return promsSettled.filter((r) => r.status === "fulfilled").length;
22 | }
23 |
24 | router.post(
25 | "/",
26 | wrap(async (req, res) => {
27 | const sources = await Source.find({}).exec();
28 | let totalNewEntries = 0;
29 | for (const s of sources) {
30 | const entries = await getFeedEntries(s.url);
31 | const savingModels = generateFeedEntries(entries, s);
32 | let newCount = await waitAndCountFulfilled(savingModels);
33 | totalNewEntries += newCount;
34 | req.log.info(
35 | `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries`
36 | );
37 | }
38 | res.json({ totalNewEntries });
39 | })
40 | );
41 |
42 | export default router;
43 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/routes/feeds.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { wrap } from "../utils.mjs";
3 | import { FeedEntry } from "../models/FeedEntry.mjs";
4 |
5 | const router = express.Router();
6 |
7 | router.get(
8 | "/",
9 | wrap(async (req, res) => {
10 | let findQuery = {};
11 | if (req.query.sourceId) {
12 | findQuery.source = req.query.sourceId;
13 | }
14 | const feedEntries = await FeedEntry.find(findQuery)
15 | .sort({ date: -1 })
16 | .limit(20)
17 | .populate("source")
18 | .exec();
19 | res.json(feedEntries);
20 | })
21 | );
22 |
23 | export default router;
24 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/routes/sources.mjs:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { Source } from "../models/Source.mjs";
3 | import { wrap } from "../utils.mjs";
4 |
5 | const router = express.Router();
6 | router.use(express.json());
7 |
8 | router.post(
9 | "/",
10 | wrap(async (req, res) => {
11 | const source = new Source({ url: req.body.url });
12 | req.log.info(`Adding new Source: ${source}`);
13 | try {
14 | await source.save();
15 | res.json(source);
16 | } catch (e) {
17 | const reason = Source.getErrorReason(e);
18 | if (reason) {
19 | req.log.error(reason);
20 | res.status(400).send();
21 | } else {
22 | req.log.error("An error occurred during Source.save()");
23 | res.status(500).send();
24 | }
25 | }
26 | })
27 | );
28 |
29 | router.get(
30 | "/",
31 | wrap(async (req, res) => {
32 | const sources = await Source.find({}).exec();
33 | res.json(sources);
34 | })
35 | );
36 |
37 | router.delete(
38 | "/:id",
39 | wrap(async (req, res) => {
40 | try {
41 | await Source.findByIdAndDelete(req.params.id).exec();
42 | res.status(200).send();
43 | } catch {
44 | req.log.error(
45 | `Error while deleting Source with id ${req.params.id}`
46 | );
47 | }
48 | })
49 | );
50 |
51 | export default router;
52 |
--------------------------------------------------------------------------------
/09-express/03-feed-api-router/utils.mjs:
--------------------------------------------------------------------------------
1 | export const wrap =
2 | (fn) =>
3 | (...args) =>
4 | fn(...args).catch(args[2]);
5 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/01-hello-fastify.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 |
6 | fastify.get("/", async function handler(request, reply) {
7 | request.log.info("Handling hello world");
8 | return { hello: "world" };
9 | });
10 |
11 | try {
12 | await fastify.listen({ port: 3000 });
13 | } catch (err) {
14 | fastify.log.error(err);
15 | process.exit(1);
16 | }
17 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/02-fastify-logging.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: {
4 | level: "debug",
5 | },
6 | requestIdLogLabel: "rid",
7 | requestIdHeader: "X-Request-ID",
8 | });
9 |
10 | fastify.get("/", async function handler(request, reply) {
11 | request.log.info("Handling hello world");
12 | return { hello: "world" };
13 | });
14 |
15 | try {
16 | await fastify.listen({ port: 3000 });
17 | } catch (err) {
18 | fastify.log.error(err);
19 | process.exit(1);
20 | }
21 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/03-short-vs-full.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 |
6 | fastify.get("/short", async function handler(request, reply) {
7 | request.log.info("Handling shorthand route");
8 | return { hello: "short" };
9 | });
10 |
11 | fastify.route({
12 | url: "/full",
13 | method: "GET",
14 | handler: async function handler(request, reply) {
15 | request.log.info("Handling full route");
16 | return { hello: "full" };
17 | },
18 | });
19 |
20 | try {
21 | await fastify.listen({ port: 3000 });
22 | } catch (err) {
23 | fastify.log.error(err);
24 | process.exit(1);
25 | }
26 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/04-params.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 |
6 | fastify.get("/hello/:name", async function handler(request, reply) {
7 | return { hello: request.params.name };
8 | });
9 | fastify.get("/any/*", async function handler(request, reply) {
10 | return { params: request.params };
11 | });
12 |
13 | try {
14 | await fastify.listen({ port: 3000 });
15 | } catch (err) {
16 | fastify.log.error(err);
17 | process.exit(1);
18 | }
19 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/05-request.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 |
6 | fastify.get("/headers", (request, reply) => {
7 | return { headers: request.headers };
8 | });
9 | fastify.get("/query", (request, reply) => {
10 | return { query: request.query };
11 | });
12 | fastify.post("/body", async function handler(request, reply) {
13 | return request.body;
14 | });
15 |
16 | try {
17 | await fastify.listen({ port: 3000 });
18 | } catch (err) {
19 | fastify.log.error(err);
20 | process.exit(1);
21 | }
22 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/06-reply-send.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 |
6 | fastify.get("/return", function handler(request, reply) {
7 | return "return";
8 | });
9 | fastify.get("/return-async", function handler(request, reply) {
10 | return "return-async";
11 | });
12 | fastify.get("/reply-send", function handler(request, reply) {
13 | reply.send("reply-send");
14 | });
15 | fastify.get("/reply-send-async", async function handler(request, reply) {
16 | reply.send("reply-send-async");
17 | return reply;
18 | });
19 | fastify.get("/return-promise", function handler(request, reply) {
20 | return Promise.resolve("return-promise");
21 | });
22 |
23 | try {
24 | await fastify.listen({ port: 3000 });
25 | } catch (err) {
26 | fastify.log.error(err);
27 | process.exit(1);
28 | }
29 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/07-reply.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 |
6 | fastify.get("/reply", function handler(request, reply) {
7 | reply
8 | .code(200)
9 | .header("Content-Type", "application/json; charset=utf-8")
10 | .send({ name: "value" });
11 | });
12 |
13 | try {
14 | await fastify.listen({ port: 3000 });
15 | } catch (err) {
16 | fastify.log.error(err);
17 | process.exit(1);
18 | }
19 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/08-errors.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 |
6 | fastify.post("/", function handler(request, reply) {
7 | return "OK";
8 | });
9 | fastify.get("/throw", function handler(request, reply) {
10 | throw new Error("Something went wrong");
11 | });
12 | fastify.get("/reject", function handler(request, reply) {
13 | return Promise.reject(new Error("Not resolved"));
14 | });
15 | fastify.get("/await-err", async function handler(request, reply) {
16 | await Promise.reject(new Error("Await error"));
17 | });
18 |
19 | try {
20 | await fastify.listen({ port: 3000 });
21 | } catch (err) {
22 | fastify.log.error(err);
23 | process.exit(1);
24 | }
25 |
--------------------------------------------------------------------------------
/10-fastify/01-essential/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "10-fastify",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "fastify": "^4.26.1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/get-feeds.mjs:
--------------------------------------------------------------------------------
1 | import { parseFeed } from "htmlparser2";
2 | import { compareDesc } from "date-fns";
3 |
4 | export default async function getFeedEntries(url, maxEntries = 5) {
5 | const feedFetched = await fetch(url);
6 | if (!feedFetched.ok) {
7 | throw new Error("Feed fetch failed");
8 | }
9 | const feedContent = await feedFetched.text();
10 | const { items } = parseFeed(feedContent);
11 | return items.slice(0, maxEntries);
12 | }
13 |
14 | export async function getLatestEntries(urls) {
15 | const feedsFetches = urls.map((s) => getFeedEntries(s));
16 | const fetchesPromises = await Promise.allSettled(feedsFetches);
17 | const retrievedFeeds = fetchesPromises
18 | .filter((r) => r.status === "fulfilled")
19 | .map((r) => r.value);
20 | const latestItems = retrievedFeeds.flat().map((i) => {
21 | return { title: i.title, link: i.link, date: i.pubDate };
22 | });
23 | latestItems.sort((a, b) => compareDesc(a.date, b.date));
24 | return latestItems;
25 | }
26 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/models/FeedEntry.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const feedEntrySchema = new Schema({
5 | source: { type: Schema.Types.ObjectId, ref: "Source" },
6 | title: { type: String },
7 | link: { type: String, unique: true },
8 | date: { type: Date },
9 | });
10 |
11 | const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema);
12 | export { FeedEntry };
13 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/models/Source.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const sourceSchema = new Schema({
5 | url: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | validate: {
10 | validator: function (v) {
11 | try {
12 | const url = new URL(v);
13 | if (!url.protocol.startsWith("http")) {
14 | return false;
15 | }
16 | } catch {
17 | return false;
18 | }
19 | return true;
20 | },
21 | message: (props) => `${props.value} is not a valid URL`,
22 | },
23 | },
24 | });
25 | sourceSchema.static("getErrorReason", (e) => {
26 | if (e.name === "ValidationError") {
27 | return "Source validation failed";
28 | } else if (e.code === 11000) {
29 | return "Feed already exists";
30 | } else {
31 | return null;
32 | }
33 | });
34 |
35 | const Source = mongoose.model("Source", sourceSchema);
36 | export { Source };
37 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "02-feed-api-fast",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@fastify/compress": "^7.0.0",
14 | "date-fns": "^3.3.1",
15 | "fastify": "^4.26.1",
16 | "htmlparser2": "^9.1.0",
17 | "mongoose": "^8.2.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/routes/feeds-update.mjs:
--------------------------------------------------------------------------------
1 | import { Source } from "../models/Source.mjs";
2 | import { FeedEntry } from "../models/FeedEntry.mjs";
3 | import getFeedEntries from "../get-feeds.mjs";
4 |
5 | function generateFeedEntries(entries, s) {
6 | return entries.map((en) => {
7 | return new FeedEntry({
8 | source: s._id,
9 | title: en.title,
10 | link: en.link,
11 | date: new Date(en.pubDate),
12 | }).save();
13 | });
14 | }
15 | async function waitAndCountFulfilled(proms) {
16 | const promsSettled = await Promise.allSettled(proms);
17 | return promsSettled.filter((r) => r.status === "fulfilled").length;
18 | }
19 |
20 | async function feedsUpdateRoutes(fastify, opts) {
21 | fastify.post("/", async (request, reply) => {
22 | const sources = await Source.find({}).exec();
23 | let totalNewEntries = 0;
24 | for (const s of sources) {
25 | const entries = await getFeedEntries(s.url);
26 | const savingModels = generateFeedEntries(entries, s);
27 | let newCount = await waitAndCountFulfilled(savingModels);
28 | totalNewEntries += newCount;
29 | request.log.info(
30 | `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries`
31 | );
32 | }
33 | return { totalNewEntries };
34 | });
35 | }
36 | export default feedsUpdateRoutes;
37 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/routes/feeds.mjs:
--------------------------------------------------------------------------------
1 | import { FeedEntry } from "../models/FeedEntry.mjs";
2 |
3 | async function feedsRoutes(fastify, opts) {
4 | fastify.get("/", async (request, reply) => {
5 | let findQuery = {};
6 | if (request.query.sourceId) {
7 | findQuery.source = request.query.sourceId;
8 | }
9 | const feedEntries = await FeedEntry.find(findQuery)
10 | .sort({ date: -1 })
11 | .limit(20)
12 | .exec();
13 | return feedEntries;
14 | });
15 | }
16 | export default feedsRoutes;
17 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/routes/sources.mjs:
--------------------------------------------------------------------------------
1 | import { Source } from "../models/Source.mjs";
2 |
3 | async function sourcesRoutes(fastify, opts) {
4 | fastify.post("/", async (request, reply) => {
5 | const source = new Source({ url: request.body.url });
6 | request.log.info(`Adding new Source: ${source}`);
7 | try {
8 | await source.save();
9 | return source;
10 | } catch (e) {
11 | const reason = Source.getErrorReason(e);
12 | if (reason) {
13 | const err = new Error(reason);
14 | err.statusCode = 400;
15 | throw err;
16 | } else {
17 | throw new Error("An error occurred during Source.save()");
18 | }
19 | }
20 | });
21 |
22 | fastify.get("/", async (request, reply) => {
23 | const sources = await Source.find({}).exec();
24 | return sources;
25 | });
26 |
27 | fastify.delete("/:id", async (request, reply) => {
28 | try {
29 | await Source.findByIdAndDelete(request.params.id).exec();
30 | return reply.code(200).send();
31 | } catch {
32 | throw new Error(
33 | `Error while deleting Source with id ${request.params.id}`
34 | );
35 | }
36 | });
37 | }
38 |
39 | export default sourcesRoutes;
40 |
--------------------------------------------------------------------------------
/10-fastify/02-feed-api-fast/server.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 | import mongoose from "mongoose";
6 | import compress from "@fastify/compress";
7 |
8 | import feedsRoutes from "./routes/feeds.mjs";
9 | import sourcesRoutes from "./routes/sources.mjs";
10 | import feedsUpdateRoutes from "./routes/feeds-update.mjs";
11 |
12 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
13 |
14 | fastify.register(compress);
15 | fastify.register(feedsRoutes, { prefix: "/feeds" });
16 | fastify.register(sourcesRoutes, { prefix: "/sources" });
17 | fastify.register(feedsUpdateRoutes, { prefix: "/feeds-update" });
18 |
19 | try {
20 | await fastify.listen({ port: 3000 });
21 | } catch (err) {
22 | fastify.log.error(err);
23 | process.exit(1);
24 | }
25 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/get-feeds.mjs:
--------------------------------------------------------------------------------
1 | import { parseFeed } from "htmlparser2";
2 | import { compareDesc } from "date-fns";
3 |
4 | export default async function getFeedEntries(url, maxEntries = 5) {
5 | const feedFetched = await fetch(url);
6 | if (!feedFetched.ok) {
7 | throw new Error("Feed fetch failed");
8 | }
9 | const feedContent = await feedFetched.text();
10 | const { items } = parseFeed(feedContent);
11 | return items.slice(0, maxEntries);
12 | }
13 |
14 | export async function getLatestEntries(urls) {
15 | const feedsFetches = urls.map((s) => getFeedEntries(s));
16 | const fetchesPromises = await Promise.allSettled(feedsFetches);
17 | const retrievedFeeds = fetchesPromises
18 | .filter((r) => r.status === "fulfilled")
19 | .map((r) => r.value);
20 | const latestItems = retrievedFeeds.flat().map((i) => {
21 | return { title: i.title, link: i.link, date: i.pubDate };
22 | });
23 | latestItems.sort((a, b) => compareDesc(a.date, b.date));
24 | return latestItems;
25 | }
26 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/models/FeedEntry.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const feedEntrySchema = new Schema({
5 | source: { type: Schema.Types.ObjectId, ref: "Source" },
6 | title: { type: String },
7 | link: { type: String, unique: true },
8 | date: { type: Date },
9 | });
10 |
11 | const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema);
12 | export { FeedEntry };
13 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/models/Source.mjs:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const { Schema } = mongoose;
3 |
4 | const sourceSchema = new Schema({
5 | url: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | validate: {
10 | validator: function (v) {
11 | try {
12 | const url = new URL(v);
13 | if (!url.protocol.startsWith("http")) {
14 | return false;
15 | }
16 | } catch {
17 | return false;
18 | }
19 | return true;
20 | },
21 | message: (props) => `${props.value} is not a valid URL`,
22 | },
23 | },
24 | });
25 | sourceSchema.static("getErrorReason", (e) => {
26 | if (e.name === "ValidationError") {
27 | return "Source validation failed";
28 | } else if (e.code === 11000) {
29 | return "Feed already exists";
30 | } else {
31 | return null;
32 | }
33 | });
34 |
35 | const Source = mongoose.model("Source", sourceSchema);
36 | export { Source };
37 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "03-feed-api-improved",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@fastify/compress": "^7.0.0",
14 | "date-fns": "^3.3.1",
15 | "fastify": "^4.26.1",
16 | "htmlparser2": "^9.1.0",
17 | "mongoose": "^8.2.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/plugins/async-utils.mjs:
--------------------------------------------------------------------------------
1 | async function asyncUtils(fastify, opts) {
2 | fastify.decorate("waitAndCountFulfilled", async function (proms) {
3 | const promsSettled = await Promise.allSettled(proms);
4 | return promsSettled.filter((r) => r.status === "fulfilled").length;
5 | });
6 | }
7 |
8 | asyncUtils[Symbol.for("skip-override")] = true;
9 | export default asyncUtils;
10 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/routes/feeds-update.mjs:
--------------------------------------------------------------------------------
1 | import { Source } from "../models/Source.mjs";
2 | import { FeedEntry } from "../models/FeedEntry.mjs";
3 | import getFeedEntries from "../get-feeds.mjs";
4 |
5 | function generateFeedEntries(entries, s) {
6 | return entries.map((en) => {
7 | return new FeedEntry({
8 | source: s._id,
9 | title: en.title,
10 | link: en.link,
11 | date: new Date(en.pubDate),
12 | }).save();
13 | });
14 | }
15 |
16 | async function feedsUpdateRoutes(fastify, opts) {
17 | fastify.post("/", async (request, reply) => {
18 | const sources = await Source.find({}).exec();
19 | let totalNewEntries = 0;
20 | for (const s of sources) {
21 | const entries = await getFeedEntries(s.url);
22 | const savingModels = generateFeedEntries(entries, s);
23 | let newCount = await fastify.waitAndCountFulfilled(savingModels);
24 | totalNewEntries += newCount;
25 | request.log.info(
26 | `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries`
27 | );
28 | }
29 | return { totalNewEntries };
30 | });
31 | }
32 | export default feedsUpdateRoutes;
33 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/routes/feeds.mjs:
--------------------------------------------------------------------------------
1 | import { FeedEntry } from "../models/FeedEntry.mjs";
2 |
3 | async function feedsRoutes(fastify, opts) {
4 | fastify.route({
5 | method: "GET",
6 | url: "/",
7 | schema: {
8 | querystring: {
9 | sourceId: { type: "string" },
10 | },
11 | response: {
12 | 200: {
13 | type: "array",
14 | items: {
15 | type: "object",
16 | properties: {
17 | source: { type: "string" },
18 | title: { type: "string" },
19 | link: { type: "string" },
20 | date: { type: "string" },
21 | },
22 | },
23 | },
24 | },
25 | },
26 | handler: async (request, reply) => {
27 | let findQuery = {};
28 | if (request.query.sourceId) {
29 | findQuery.source = request.query.sourceId;
30 | }
31 | const feedEntries = await FeedEntry.find(findQuery)
32 | .sort({ date: -1 })
33 | .limit(20)
34 | .exec();
35 | return feedEntries;
36 | },
37 | });
38 | }
39 | export default feedsRoutes;
40 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/routes/sources.mjs:
--------------------------------------------------------------------------------
1 | import { Source } from "../models/Source.mjs";
2 |
3 | async function sourcesRoutes(fastify, opts) {
4 | const postFeedsSchema = {
5 | body: {
6 | type: "object",
7 | required: ["url"],
8 | properties: {
9 | url: { type: "string" },
10 | },
11 | },
12 | };
13 | fastify.post("/", { schema: postFeedsSchema }, async (request, reply) => {
14 | const source = new Source({ url: request.body.url });
15 | request.log.info(`Adding new Source: ${source}`);
16 | try {
17 | await source.save();
18 | return source;
19 | } catch (e) {
20 | const reason = Source.getErrorReason(e);
21 | if (reason) {
22 | const err = new Error(reason);
23 | err.statusCode = 400;
24 | throw err;
25 | } else {
26 | throw new Error("An error occurred during Source.save()");
27 | }
28 | }
29 | });
30 |
31 | const getSourcesSchema = {
32 | response: {
33 | 200: {
34 | type: "array",
35 | items: {
36 | type: "object",
37 | properties: {
38 | _id: { type: "string" },
39 | url: { type: "string" },
40 | },
41 | },
42 | },
43 | },
44 | };
45 | fastify.get("/", { schema: getSourcesSchema }, async (request, reply) => {
46 | const sources = await Source.find({}).exec();
47 | return sources;
48 | });
49 |
50 | fastify.delete("/:id", async (request, reply) => {
51 | try {
52 | await Source.findByIdAndDelete(request.params.id).exec();
53 | return reply.code(200).send();
54 | } catch {
55 | throw new Error(
56 | `Error while deleting Source with id ${request.params.id}`
57 | );
58 | }
59 | });
60 | }
61 |
62 | export default sourcesRoutes;
63 |
--------------------------------------------------------------------------------
/10-fastify/03-feed-api-improved/server.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from "fastify";
2 | const fastify = Fastify({
3 | logger: true,
4 | });
5 | import mongoose from "mongoose";
6 | import compress from "@fastify/compress";
7 |
8 | import feedsRoutes from "./routes/feeds.mjs";
9 | import sourcesRoutes from "./routes/sources.mjs";
10 | import feedsUpdateRoutes from "./routes/feeds-update.mjs";
11 | import asyncUtils from "./plugins/async-utils.mjs";
12 |
13 | await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader");
14 |
15 | fastify.addHook("onSend", async (request, reply, payload) => {
16 | reply.header("server", "fastify");
17 | });
18 | fastify.register(compress);
19 | fastify.register(asyncUtils);
20 | fastify.register(feedsRoutes, { prefix: "/feeds" });
21 | fastify.register(sourcesRoutes, { prefix: "/sources" });
22 | fastify.register(feedsUpdateRoutes, { prefix: "/feeds-update" });
23 |
24 | try {
25 | await fastify.listen({ port: 3000 });
26 | } catch (err) {
27 | fastify.log.error(err);
28 | process.exit(1);
29 | }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node.js - Guida per creare API e applicazioni in JavaScript
2 |
3 |
4 |
5 | Questo è il repository di Node.js - Guida per creare API e applicazioni in JavaScript pubblicato da Apogeo Editore . All'interno trovate il codice sviluppato in tutti i capitoli del libro.
6 |
7 | **Un percorso alla scoperta di Node.js un passo alla volta, partendo dal basso e dalle sue funzioni più semplici fino ad arrivare a quelle più complesse.**.
8 |
9 | ### [🔗 Sito ufficiale](https://node-js.miliucci.org)
10 |
11 | ### [🐞 Errata](https://github.com/lifeisfoo/node-js/discussions/categories/errori-e-refusi)
12 |
13 | ## Spazio per i lettori
14 |
15 | È disponibile uno spazio [Discussions su Github](https://github.com/lifeisfoo/node-js/discussions) per fare domande e discutere con gli altri lettori. In caso di problemi con i passaggi descritti nel libro è possibile chiedere aiuto.
16 |
17 | ## Supporta il libro
18 |
19 | Scrivere un libro è un'attività che richiede molto tempo e lavoro. Se grazie a questo libro avete imparato qualcosa di nuovo, o se l'avete trovato utile, potete supportarlo **lasciando una recensione online su Amazon o sul social network che utilizzate**.
20 |
21 | ## Acquista il libro
22 |
23 | È possibile acquistare il libro:
24 |
25 | - direttamente dal sito dell'editore [Apogeo](https://www.apogeonline.com/libri/node-js-alessandro-miliucci/)
26 | - su [Amazon.it](https://www.amazon.it/dp/8850336829/)
27 | - su [IBS.it](https://www.ibs.it/nodejs-guida-per-creare-api-libro-alessandro-miliucci/e/9788850336821)
28 | - su [la Feltrinelli.it](https://www.lafeltrinelli.it/nodejs-guida-per-creare-api-libro-alessandro-miliucci/e/9788850336821)
29 | - nelle principali librerie fisiche e virtuali
30 |
31 | ---
32 |
33 | Alessandro Miliucci [🔗Linkedin](https://www.linkedin.com/in/alessandro-miliucci/) [🔗 Web](https://miliucci.org)
34 |
--------------------------------------------------------------------------------