├── .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 |
    18 | 37 |

    Page Heading with Italics and Underline

    38 |

    Page Subheading with highlighting

    39 |
    40 |

    41 | Italic Link ButtonBold Link Button → 43 |

    44 |
    45 |
    46 |
    47 |
    48 |
    49 |

    Section Heading

    50 |

    Section Subheading

    51 |
    52 | 57 | 61 | 65 |
    66 |
    67 |
    68 |
    69 | "Quote" 70 |
    - Attribution
    71 |
    72 |
    73 |
    74 |
    75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
    Col ACol BCol C
    Row 1Cell A1Cell B1Cell C1
    Row 2Cell A2Cell B2Cell C2
    97 |
    98 |
    99 |
    100 |

    Left-aligned header

    101 |

    Left-aligned paragraph

    102 | 105 | 109 |
    110 | Stock photo 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 |
    137 |
    138 |
    139 |

    Form title

    140 |
    141 | 142 | 149 | 150 | 154 | 155 | 156 | 157 |
    158 |
    159 |
    160 | 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 |
    10 | 17 |
    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 | --------------------------------------------------------------------------------