├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .husky └── pre-commit ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── Readme.md ├── Readme.tl ├── SECURITY.md ├── assets ├── benchmark.webp └── imgs │ ├── Untitled-2023-07-28-1312.png │ ├── call-stack.png │ ├── client-server.png │ ├── compressed │ ├── Untitled-2023-07-28-1312.png │ ├── call-stack.png │ ├── client-server.png │ ├── cover.jpg │ ├── dynamic_router_trie.webp │ ├── elysia-claim.png │ ├── hello-world.png │ ├── idle_memory.png │ ├── intellisense.png │ ├── latency_2.png │ ├── latency_without_max.png │ ├── max_latency.png │ ├── mem_const_load.png │ ├── mem_const_load_2.png │ ├── mem_idle_2.png │ ├── modular-function-ch6.png │ ├── next.png │ ├── prev.png │ ├── referer.png │ ├── rps.png │ ├── rps_2.png │ ├── trie-eow.png │ ├── trie-overview.png │ └── user-agent.png │ ├── cover.jpg │ ├── dynamic_router_trie.webp │ ├── elysia-claim.png │ ├── hello-world.png │ ├── idle_memory.png │ ├── intellisense.png │ ├── latency_2.png │ ├── latency_without_max.png │ ├── max_latency.png │ ├── mem_const_load.png │ ├── mem_const_load_2.png │ ├── mem_idle_2.png │ ├── modular-function-ch6.png │ ├── next.png │ ├── prev.png │ ├── referer.png │ ├── rps.png │ ├── rps_2.png │ ├── trie-eow.png │ ├── trie-overview.png │ └── user-agent.png ├── chapters ├── .md ├── ch00-nodejs-faster-than-you-think.md ├── ch01.0-what-is-a-web-server-anyway.md ├── ch02.0-your-first-nodejs-program.md ├── ch03.0-working-with-files.md ├── ch04.0-logtar-our-logging-library.md ├── ch04.1-refactoring-the-code.md ├── ch04.2-writing-logs.md ├── ch04.3-capturing-metadata.md ├── ch04.4-intro-to-async-vs-sync.md ├── ch04.5-rolling-file-support.md ├── ch05.0-http-deep-dive.md ├── ch05.1-http-verbs-versioning-http1_1.md ├── ch05.2-user-agents.md ├── ch05.3-mime-type-and-content-type.md ├── ch05.4-headers.md ├── ch05.5-response-status-codes.md ├── ch06.00-velocy-our-backend-framework.md ├── ch06.01-basic-router-implementation.md ├── ch06.02-the-router-class.md ├── ch06.03-improving-the-router-api.md ├── ch06.04-the-need-for-a-trie.md ├── ch06.05-ex-implementing-a-trie.md ├── ch06.06-ex-implementing-router.md ├── ch06.07-ex-adding-http-methods.md ├── ch06.08-adding-verbs-api.md ├── ch06.09-ex-dynamic-routing.md ├── ch06.10-running-our-server.md ├── ch06.11-building-a-web-server.md └── ch06.12-query-parameters.md ├── genIndex.js ├── new-in-this-release.log ├── package-lock.json ├── package.json └── src ├── README.md ├── chapter_04.0 ├── README.md └── index.js ├── chapter_04.1 ├── README.md ├── index.js ├── lib │ ├── config │ │ ├── log-config.js │ │ └── rolling-config.js │ ├── logger.js │ ├── logtar.js │ └── utils │ │ ├── log-level.js │ │ └── rolling-options.js └── package.json ├── chapter_04.2 ├── README.md ├── config.json ├── index.js ├── lib │ ├── config │ │ ├── log-config.js │ │ └── rolling-config.js │ ├── logger.js │ ├── logtar.js │ └── utils │ │ ├── helpers.js │ │ ├── log-level.js │ │ └── rolling-options.js ├── logs │ └── LogTar_2023-08-18T19-48-03.log ├── package-lock.json └── package.json ├── chapter_04.3 ├── README.md ├── index.js ├── lib │ ├── config │ │ ├── log-config.js │ │ └── rolling-config.js │ ├── logger.js │ ├── logtar.js │ └── utils │ │ ├── helpers.js │ │ ├── log-level.js │ │ └── rolling-options.js ├── logs │ └── Logtar_2023-08-19T19-11-51.log ├── package-lock.json └── package.json ├── chapter_04.5 ├── .gitignore ├── .vscode │ └── settings.json ├── README.md ├── config.json ├── index.js ├── lib │ ├── config │ │ ├── log-config.js │ │ └── rolling-config.js │ ├── logger.js │ ├── logtar.js │ └── utils │ │ ├── helpers.js │ │ ├── log-level.js │ │ └── rolling-options.js ├── logs │ ├── Logtar_2023-08-22T00-41-58.log │ ├── Logtar_2023-08-22T00-41-59.log │ ├── Logtar_2023-08-22T00-42-01.log │ ├── Logtar_2023-08-22T00-42-02.log │ ├── Logtar_2023-08-22T00-42-03.log │ ├── Logtar_2023-08-22T00-42-04.log │ ├── Logtar_2023-08-22T00-42-05.log │ ├── Logtar_2023-08-22T00-42-06.log │ ├── Logtar_2023-08-22T00-42-07.log │ └── Logtar_2023-08-22T00-42-08.log └── package.json ├── chapter_06.01 └── index.js ├── chapter_06.02 └── index.js ├── chapter_06.03 └── index.js ├── chapter_06.05 ├── challenge_1.js └── challenge_2.js ├── chapter_06.06 ├── challenge_1.js └── challenge_2.js ├── chapter_06.07 └── index.js ├── chapter_06.08 └── index.js ├── chapter_06.09 └── index.js ├── chapter_06.10 ├── globals.js └── index.js ├── chapter_06.11 ├── globals.js └── lib │ ├── constants.js │ ├── index.js │ └── router.js └── chapter_06.12 ├── challenge1 ├── globals.js └── lib │ ├── constants.js │ ├── index.js │ └── router.js └── challenge2 ├── globals.js └── lib ├── constants.js ├── index.js ├── router.js └── utils.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Is this issue already raised? Yes/No 2 | 3 | ## Chapter: 4 | 5 | ## Section Title: 6 | 7 | ## Topic: 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test.md 3 | generate-index.js 4 | test.js 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | node genIndex.js 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hello@isht.dev. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | I'm thrilled that you're interested in contributing to this open-source book! Your contributions will help improve the quality and impact of the content for the benefit of the community. Please take a moment to read through the guidelines outlined below before you start contributing. 3 | 4 | ## How to Contribute 5 | You can contribute to this open-source book by submitting Pull Requests (PRs) for various improvements, such as code snippets, explanations, clarifications, and more. We appreciate your efforts in helping to make this resource better. 6 | 7 | If you're unsure about a particular word, phrase, or concept used in the book, or if you have any doubts, please consider opening an issue to discuss it before creating a PR. We believe that collaboration and open communication lead to better results. 8 | 9 | ## Content Contributions 10 | By contributing content to this repository, you acknowledge and agree to the following: 11 | 12 | **License**: Any content you contribute to this repository is provided under the terms of the MIT license. This means that you grant the repository owner a non-exclusive license to use, modify, and distribute your contributed content for the purposes of the book. 13 | 14 | **Non-Exclusive**: This license is non-exclusive, which means you retain the right to use your contributed content elsewhere. 15 | 16 | **Attribution**: Your contributions will be appropriately attributed to you within the book, in the contributors section. 17 | 18 | ## Getting Started 19 | 1. Fork the repository to your GitHub account. 20 | 21 | 2. Create a new branch for your contributions. This helps in isolating changes and making your PRs more manageable. 22 | 23 | 3. Make your desired changes or additions to the content. 24 | 25 | 4. Test your changes to ensure they fit seamlessly with the existing content. For example, hyperlinks. 26 | 27 | 5. Commit your changes with clear and descriptive commit messages. 28 | 29 | 6. Create a Pull Request (PR) from your forked repository's branch to the main repository's branch. 30 | 31 | 7. Provide a (somewhat) detailed description of your changes in the PR, along with any relevant context. 32 | 33 | 8. Engage in any discussions or feedback related to your PR. Be open to suggestions and improvements. 34 | 35 | 9. Once your PR is approved, it will be merged into the main repository. 36 | 37 | ## Code of Conduct 38 | By participating in this project, you agree to adhere to the [Code of Conduct](/CODE_OF_CONDUCT.md). We aim to maintain a respectful and inclusive environment for all contributors. 39 | 40 | Thank you for being a part of Learn Nodejs the Hard way! Your contributions make a difference in the community. 41 | -------------------------------------------------------------------------------- /Readme.tl: -------------------------------------------------------------------------------- 1 | # Learn Node.js by building a backend framework - [Velocy](https://github.com/ishtms/velocy) 2 | 3 |

4 | Learn nodejs the hard way 5 |

6 | 7 | You can access the current version of the book in the [chapters directory](/chapters) or in PDF format (both Light and Dark modes are available) by [clicking here](https://github.com/ishtms/learn-nodejs-hard-way/releases). Note that this version includes the current release of the content, and is not the final version. 8 | 9 | > This book is still in a very early stage. It contains an insignificant portion of the total content that the book is supposed to cover. There’s going to be 0 dependencies for our [backend framework](https://github.com/ishtms/velocy), as well as our [logging library](https://github.com/ishtms/logtar). Everything will be done using vanilla Node.js, the hard-way (the best way to learn). 10 | 11 | --- 12 | 13 | ## Note 14 | 15 | If you're not familiar with javascript, you may also check out my other repository - [Learn Javascript - The Easy Way](https://github.com/ishtms/learn-javascript-easy-way) that takes you on a deep and a fun journey into Javascript - from the very basics to the advanced concepts that you'd ever need, without diving into too much theory. Only practical code examples. 16 | 17 | --- 18 | 19 | To master a new concept, it's often best to begin from the ground up. This isn't just another Node.js guide; it's a comprehensive, code-along experience aimed at building a real world product that may be used by thousands of developers. The product that we're going to build will be a backend framework, that too from scratch. 20 | 21 | You won't just learn how Node.js works, but also why it operates in a particular way. The guide also includes discussions on relevant data structures and design patterns. 22 | 23 | The book also includes a wide range of exercises specifically created to challenge you, that may require commitment and consistent effort on your part. The first exercises start from [chapter 7](/chapters/ch06.5-ex-implementing-a-trie.md) 24 | 25 | This guide goes beyond the basics. We're focused on delivering a modular, optimized backend framework that is close to being production-ready. Topics like performance optimization, security measures, and various testing approaches will be covered to ensure the framework is both reliable and extendable. 26 | 27 | I highly recommend actively coding alongside this guide, rather than just reading through it, for a full understanding of Node.js and its more intricate aspects. 28 | 29 | The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy). (W.I.P) 30 | 31 | [![Read Next](/assets/imgs/next.png)](/chapters/ch01.0-what-is-a-web-server-anyway.md) 32 | 33 | {{toc}} 34 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Issues 2 | If you discover a security vulnerability or any issue with the code in the book, that could potentially compromise the security of users, please follow these steps: 3 | 4 | * **Privately Notify Us**: Directly message or email us at hello@isht.dev with the details of the security issue. Do not disclose the issue publicly until we have had a chance to address it. 5 | 6 | * **Provide Details**: Please include the nature of the issue, the steps to reproduce it, and any relevant information that could help us understand and resolve the problem. 7 | 8 | * **Be Patient**: We will acknowledge your report as soon as possible, and we will work diligently to address the issue within a reasonable timeframe. We will keep you updated on our progress. 9 | 10 | ## Scope 11 | Please note that this security policy only applies to the content and code within the [learn-nodejs-hard-way](https://github.com/ishtms/learn-nodejs-hard-way) repo. It does not cover any tools created in the book. Please report issues on their own github repositories. 12 | 13 | ## Responsible Disclosure 14 | We kindly request that you follow responsible disclosure practices: 15 | 16 | * Give us a reasonable amount of time to address the issue before public disclosure. 17 | * Do not exploit the issue for malicious purposes or to gain unauthorized access to systems. 18 | 19 | ## Acknowledgement 20 | We appreciate the efforts of security researchers and the community in helping to improve the security of the content. As a token of our gratitude, we will acknowledge your contribution in the book's acknowledgments section (unless you prefer to remain anonymous). 21 | 22 | Your commitment to security is essential to maintaining the integrity of the this book. Thank you for working with us to make this open-source book safer for everyone. 23 | 24 | Please note that this security policy is subject to change without notice. It was last updated on 22 Aug 2023. If you have any questions or concerns, please contact us at hello@isht.dev. 25 | -------------------------------------------------------------------------------- /assets/benchmark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/benchmark.webp -------------------------------------------------------------------------------- /assets/imgs/Untitled-2023-07-28-1312.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/Untitled-2023-07-28-1312.png -------------------------------------------------------------------------------- /assets/imgs/call-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/call-stack.png -------------------------------------------------------------------------------- /assets/imgs/client-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/client-server.png -------------------------------------------------------------------------------- /assets/imgs/compressed/Untitled-2023-07-28-1312.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/Untitled-2023-07-28-1312.png -------------------------------------------------------------------------------- /assets/imgs/compressed/call-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/call-stack.png -------------------------------------------------------------------------------- /assets/imgs/compressed/client-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/client-server.png -------------------------------------------------------------------------------- /assets/imgs/compressed/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/cover.jpg -------------------------------------------------------------------------------- /assets/imgs/compressed/dynamic_router_trie.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/dynamic_router_trie.webp -------------------------------------------------------------------------------- /assets/imgs/compressed/elysia-claim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/elysia-claim.png -------------------------------------------------------------------------------- /assets/imgs/compressed/hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/hello-world.png -------------------------------------------------------------------------------- /assets/imgs/compressed/idle_memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/idle_memory.png -------------------------------------------------------------------------------- /assets/imgs/compressed/intellisense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/intellisense.png -------------------------------------------------------------------------------- /assets/imgs/compressed/latency_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/latency_2.png -------------------------------------------------------------------------------- /assets/imgs/compressed/latency_without_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/latency_without_max.png -------------------------------------------------------------------------------- /assets/imgs/compressed/max_latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/max_latency.png -------------------------------------------------------------------------------- /assets/imgs/compressed/mem_const_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/mem_const_load.png -------------------------------------------------------------------------------- /assets/imgs/compressed/mem_const_load_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/mem_const_load_2.png -------------------------------------------------------------------------------- /assets/imgs/compressed/mem_idle_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/mem_idle_2.png -------------------------------------------------------------------------------- /assets/imgs/compressed/modular-function-ch6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/modular-function-ch6.png -------------------------------------------------------------------------------- /assets/imgs/compressed/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/next.png -------------------------------------------------------------------------------- /assets/imgs/compressed/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/prev.png -------------------------------------------------------------------------------- /assets/imgs/compressed/referer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/referer.png -------------------------------------------------------------------------------- /assets/imgs/compressed/rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/rps.png -------------------------------------------------------------------------------- /assets/imgs/compressed/rps_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/rps_2.png -------------------------------------------------------------------------------- /assets/imgs/compressed/trie-eow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/trie-eow.png -------------------------------------------------------------------------------- /assets/imgs/compressed/trie-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/trie-overview.png -------------------------------------------------------------------------------- /assets/imgs/compressed/user-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/compressed/user-agent.png -------------------------------------------------------------------------------- /assets/imgs/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/cover.jpg -------------------------------------------------------------------------------- /assets/imgs/dynamic_router_trie.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/dynamic_router_trie.webp -------------------------------------------------------------------------------- /assets/imgs/elysia-claim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/elysia-claim.png -------------------------------------------------------------------------------- /assets/imgs/hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/hello-world.png -------------------------------------------------------------------------------- /assets/imgs/idle_memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/idle_memory.png -------------------------------------------------------------------------------- /assets/imgs/intellisense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/intellisense.png -------------------------------------------------------------------------------- /assets/imgs/latency_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/latency_2.png -------------------------------------------------------------------------------- /assets/imgs/latency_without_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/latency_without_max.png -------------------------------------------------------------------------------- /assets/imgs/max_latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/max_latency.png -------------------------------------------------------------------------------- /assets/imgs/mem_const_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/mem_const_load.png -------------------------------------------------------------------------------- /assets/imgs/mem_const_load_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/mem_const_load_2.png -------------------------------------------------------------------------------- /assets/imgs/mem_idle_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/mem_idle_2.png -------------------------------------------------------------------------------- /assets/imgs/modular-function-ch6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/modular-function-ch6.png -------------------------------------------------------------------------------- /assets/imgs/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/next.png -------------------------------------------------------------------------------- /assets/imgs/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/prev.png -------------------------------------------------------------------------------- /assets/imgs/referer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/referer.png -------------------------------------------------------------------------------- /assets/imgs/rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/rps.png -------------------------------------------------------------------------------- /assets/imgs/rps_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/rps_2.png -------------------------------------------------------------------------------- /assets/imgs/trie-eow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/trie-eow.png -------------------------------------------------------------------------------- /assets/imgs/trie-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/trie-overview.png -------------------------------------------------------------------------------- /assets/imgs/user-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/assets/imgs/user-agent.png -------------------------------------------------------------------------------- /chapters/.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishtms/learn-nodejs-hard-way/24a117e0877b63a50115ba6e87276f626c5cbf88/chapters/.md -------------------------------------------------------------------------------- /chapters/ch04.4-intro-to-async-vs-sync.md: -------------------------------------------------------------------------------- 1 | [![Read Prev](/assets/imgs/prev.png)](/chapters/ch04.3-capturing-metadata.md) 2 | 3 | ## A small intro to `async` vs `sync` 4 | 5 | 6 | 7 | > Note: I am not going to explain the event loop just yet. We will have a dedicated chapter on it later in this book. Bear with me whenever I say "event-loop.”. 8 | 9 | > As an asynchronous event-driven JavaScript runtime, Node.js is designed to build scalable network applications 10 | 11 | The line above is the very first line you'd see on the [About Node.js](https://nodejs.org/en/about) page. Let's understand what do they mean by \***\*asynchronous event-driven\*\*** runtime. 12 | 13 | In Node.js, doing things the asynchronous way is a very common approach due to its efficiency in handling multiple tasks at once. However, this approach can be complex and requires a careful understanding of the interplay between asynchronous (async) and synchronous (sync) operations. 14 | 15 | This chapter aims to provide a comprehensive explanation of how these operations work together in Node.js. We will delve into the intricacies of async and sync operations, including how buffers can be used to fine-tune file calls. 16 | 17 | Also, we will explore Node.js's **smart** optimization techniques, which allow for increased performance and responsiveness. By understanding the interplay between async and sync operations, the role of buffers, and the trade-offs involved in optimization, you will be better equipped to write efficient and effective Node.js applications. 18 | 19 | ### The Balance between Opposites 20 | 21 | Node.js's architecture is designed to support asynchronous operations, which is consistent with JavaScript's event-driven nature. Asynchronous operations can be executed via callbacks, Promises, and async/await. This concurrency allows tasks to run in parallel (not exactly parallel like multi-threading), which enables your application to remain responsive even during resource-intensive operations such as file I/O. 22 | 23 | However, synchronous operations disrupts this balance. When a synchronous operation is encountered, the entire code execution is halted until the operation completes. Although synchronous operations can usually execute more quickly due to their direct nature, they can also lead to bottlenecks and even application unresponsiveness, particularly under high I/O workloads. 24 | 25 | ### Mixing Asynchronous and Synchronous Code 26 | 27 | You must ensure consistency between asynchronous and synchronous operations. Combining these paradigms can lead to a lot of challenges. The use of synchronous operations within an asynchronous context can result in performance bottlenecks, which can derail the potential of your application. 28 | 29 | Every operation affects how quickly it responds to requests. If you use both async and sync operations, it can make the application slower and less efficient. 30 | 31 | ### Faster I/O out of the box 32 | 33 | Node.js uses buffering to handle file operations. Instead of writing data directly to the disk, Node.js stores the data in an internal buffer in memory. This buffer combines multiple write operations and writes them to the disk as one entity, which is more efficient. This strategy has two benefits: it's much faster to write data to memory than to disk, and batching write operations reduces the number of disk I/O requests, which saves time. 34 | 35 | Node.js's internal buffering mechanism can make asynchronous write operations feel instantaneous, as they are merely appending data to memory without the overhead of immediate disk writes. However, it's important to note that this buffered data isn't guaranteed to be persisted until it's flushed to the disk. 36 | 37 | ### Blocking Code 38 | 39 | Blocking code refers to a situation where additional JavaScript execution in the Node.js process has to wait until a non-JavaScript operation finishes. This can occur because the event loop cannot continue running JavaScript when a blocking operation is in progress. 40 | 41 | Consider the following example that reads a file synchronously: 42 | 43 | ```js 44 | const fs = require("node:fs"); 45 | 46 | // blocks the entire program till this finishes 47 | const fileData = fs.readFileSync("/path/to/file"); 48 | console.log(fileData); 49 | ``` 50 | 51 | The `readFileSync` method is blocking, which means that the JavaScript execution in the Node.js process has to wait until the file reading operation is complete before continuing. This can cause performance issues, especially when dealing with large files or when multiple blocking operations are performed in sequence. 52 | 53 | Fortunately, the Node.js standard library offers asynchronous versions of all I/O methods, which are non-blocking and accept callback functions. Consider the following example: 54 | 55 | ```js 56 | const fs = require("node:fs/promises"); 57 | 58 | async function some_function() { 59 | // blocks the execution of the current function 60 | // but other tasks are unaffected 61 | const data = await fs.readFile("test.js", "utf-8"); 62 | console.log(data); 63 | } 64 | 65 | some_function(); 66 | ``` 67 | 68 | The `readFile` method is non-blocking, which means that the JavaScript execution can continue running while the file reading operation is in progress. When the operation is complete, the next line is executed. 69 | 70 | Some methods in the Node.js standard library also have blocking counterparts with names that end in `Sync`. For example, `readFileSync`, that we just saw, has a non-blocking counterpart called `readFile`. 71 | 72 | It's important to understand the difference between blocking and non-blocking operations in Node.js to write efficient and performant code. 73 | 74 | ### Concurrency 75 | 76 | One key aspect of Node.js is that JavaScript execution is single-threaded, meaning that only one task can be executed at a time. This can be a challenge when dealing with tasks that require a lot of processing power or that involve I/O operations, which can cause the program to block and slow down its execution. 77 | 78 | To address this issue, Node.js uses an event-driven, non-blocking I/O model. This means that instead of waiting for an I/O operation to complete before moving on to the next task, Node.js can perform other tasks while the I/O operation is taking place. 79 | 80 | For example, suppose an average request for an API endpoint takes 60ms. Within that 60ms, 30ms are spent reading from a database, and 25ms are spent reading from a file. If these process were synchronous, our web server would be incapable of handling a large number of concurrent requests. However, Node.js solves this problem with its non-blocking model. If you use the asynchronous version of the file/database API, the operation will continue to serve other requests whenever it needs to wait for the database or file. 81 | 82 | [![Read Next](/assets/imgs/next.png)](/chapters/ch04.5-rolling-file-support.md) 83 | -------------------------------------------------------------------------------- /chapters/ch05.2-user-agents.md: -------------------------------------------------------------------------------- 1 | [![Read Prev](/assets/imgs/prev.png)](/chapters/ch05.1-http-verbs-versioning-http1_1.md) 2 | 3 | ## User agents 4 | 5 | 6 | 7 | A user agent is any software client that sends a request for data or information. One of the most common type of user agent are the web browsers. There are many other types of user agents as well. These include crawlers (web-traversing robots), command-line tools, billboard screens, household appliances, scales, light bulbs, firmware update scripts, mobile apps, and communication devices in various shapes and sizes. 8 | 9 | It's worth noting that the term "user agent" doesn't imply a human user is interacting with the software agent at the time of the request. In fact, many user agents are installed or configured to run in the background and save their results for later inspection or to only save a subset of results that might be interesting or erroneous. For instance, web-crawlers are typically given a starting URL and programmed to follow specific behavior while crawling the web as a hypertext graph. 10 | 11 | In our `cURL` response, we have the user-agent listed on this line: 12 | 13 | ```bash 14 | → User-Agent: curl/7.87.0 15 | ``` 16 | 17 | Here, the user-agent is the `cURL` command line program that we used to make the request to our server. But, it get's more strange when you make a request from a browser. Let's see that in action. 18 | 19 | ### `User-Agent` can be weird 20 | 21 | Open chrome or any browser of your choice, and open the web console using `Command + Option + J` on a mac or `Control + Shift + J` on windows or linux. Type the following command in the console and press enter: 22 | 23 | ```js 24 | navigator.userAgent; 25 | ``` 26 | 27 | This outputs: 28 | 29 | ![](/assets/imgs/user-agent.png) 30 | 31 | Ha, such a weird looking string isn't it? What are 3 browsers doing there? Shouldn't it be only `Chrome`? 32 | 33 | This phenomenon of "pretending to be someone else" is quite prevalent in the browser world, and almost all browsers have adopted this strategy for practical reasons. A great article that you should read if you're interested is the [user-agent-string-history](https://webaim.org/blog/user-agent-string-history/). 34 | 35 | For instance, let's examine the example in question: 36 | 37 | ```bash 38 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 39 | ``` 40 | 41 | It is a representation of Chrome 116 on a Macintosh. But why is it showing Mozilla, even though we are not using it? 42 | 43 | ```bash 44 | Why does a browser behave that way? 45 | ``` 46 | 47 | The answer is simple. Many websites and web pages are designed to identify which browser is visiting the page, and only deliver certain features if the browser is Chrome/Mozilla/... (although this is a BAD programming practice, (we'll get to it in a bit) it happens, especially in the early days of the Internet). In such cases, if a new browser is released and wants to provide the same features for that page, it must pretend to be Chrome/Mozilla/Safari 48 | 49 | _Does that mean that I cannot find the information I need from the `User-Agent` header?_ 50 | 51 | Not at all. You can definitely find the information you need, but you must match the `User-Agent` precisely with the User-Agent database. This can be a daunting task because there are more than [**219 million**](https://explore.whatismybrowser.com/useragents/explore/) user agent strings! 52 | 53 | Like I told earlier, checking the type of browser to do things separately is a very bad idea. Some reasons include: 54 | 55 | - Malicious clients can easily manipulate or spoof user agents, causing the information provided in the user agent string to be potentially inaccurate. 56 | - Modern browsers often use similar user agent structures to maintain compatibility with legacy websites, making it difficult to accurately identify specific browsers or features based solely on user agents. 57 | - The rise of mobile devices and Internet of Things (IoT) devices has led to a diverse range of user agents, making it challenging to accurately identify and categorize all types of devices. 58 | - Some users might switch user agents to access restricted content or to avoid being tracked, resulting in unexpected behavior when making decisions based on user agent data. 59 | - Many alternative and lesser-known browsers and user agents do not follow standard patterns, so relying solely on well-known user agents could exclude valid users. 60 | - Tying application behavior too closely to specific user agents can hinder future updates or improvements and complicate the development process by requiring constant adjustments. 61 | 62 | You can find a very detailed explanation on why this is bad here - [Browser detection using the user agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent) and what alternate steps you could take in order to create a feature that's not supported on some browsers. 63 | 64 | [![Read Next](/assets/imgs/next.png)](/chapters/ch05.3-mime-type-and-content-type.md) 65 | -------------------------------------------------------------------------------- /chapters/ch05.3-mime-type-and-content-type.md: -------------------------------------------------------------------------------- 1 | [![Read Prev](/assets/imgs/prev.png)](/chapters/ch05.2-user-agents.md) 2 | 3 | ## MIME Type and `Content-Type` 4 | 5 | 6 | 7 | In this chapter, we're going to take a look at the next line of the `cURL` output: 8 | 9 | ```bash 10 | > Accept: */* 11 | ``` 12 | 13 | This line is specifies a header named `Accept` which has the value of `*/*`. This is very crucial. Let's take a moment to understand, why. 14 | 15 | ### Understanding the `Accept` Header 16 | 17 | The **`Accept`** header is important in an HTTP request, especially when your client (in this case, `cURL`) wants to tell the server what types of media it can handle. This header tells the server the types of media or MIME types that the client wants in the response body. Essentially, it's like the client's way of saying, "I'm open to receiving content in these formats." 18 | 19 | #### Breaking Down the Line 20 | 21 | In the cURL output, the line **`> Accept: */*`** might look confusing, but it means something simple. Let's break it down: 22 | 23 | - **`>`**: This symbol shows that the line is part of the request headers sent from the client (you, via cURL) to the server. 24 | - **`Accept: */*`**: This is the actual **`Accept`** header value. The `*/*` part means "anything and everything." It shows that the client is willing to accept any type of media or MIME type in the response. In other words, the server has the freedom to choose the most suitable format to send back. 25 | 26 | #### Why the Wildcard? 27 | 28 | You might wonder why the client would use such a general approach. The reason is flexibility. By using **`*/*`**, the client is showing that it can handle many content types. This can be useful when the client doesn't care about the specific format or when it's okay with multiple formats. The server can then choose the most appropriate representation of the resource based on factors like its capabilities and available content types. 29 | 30 | #### Server Response 31 | 32 | Based on the **`Accept: */*`** header, the server should create a response that matches the client's willingness to accept any media type. The server chooses the most suitable **Content-Type** from its available options and include it in the response headers. 33 | 34 | ### Mime Type 35 | 36 | You are likely already familiar with MIME types if you have engaged in web development, particularly when including JavaScript files into HTML documents. For example, the line below might look familiar: 37 | 38 | ```html 39 | 40 | ``` 41 | 42 | The `type` attribute on the script tag above is also a MIME type. It consists of two parts - a `type` i.e **text** and a `subtype` i.e **javascript**. 43 | 44 | **MIME type**(s) are a critical part of how the web works. It stands for Multipurpose Internet Mail Extensions, and are often called "media types". They function like tags that are attached to the content shared across the internet, in order to provide information about the type of data contained within them. This information allows browsers and applications to properly process and display the content to the user. 45 | 46 | For example, a plain text document may have a different MIME type than an image or an audio file. Additionally, even within the same category, such as images or audio files, there may be different formats that require different MIME types. This is because each file format has unique characteristics that need to be accounted for in order to ensure proper display and functionality. 47 | 48 | For example, the MIME type `image/jpeg` means the file has a JPEG image and `audio/mp3` means the file has an MP3 audio. These labels are important so that web browsers can display content correctly and multimedia players can play the right kind of media file. 49 | 50 | Without them, web pages would be confusing and multimedia files wouldn't work right. To make sure files work right, we include the right MIME type label when uploading to a website or sending them through email. 51 | 52 | You can find a exhaustive list of all the MIME types on [Media container formats (file types)](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers) 53 | 54 | ### Anatomy of a MIME type 55 | 56 | A MIME type has two parts: a "type" and a "subtype." These parts are separated by a slash ("/") and have no spaces. The "type" tells you **what category the data belongs** to, like "video" or "text." The "subtype" tells you exactly **what kind of data it is**, like "plain" for plain text or "html" for HTML source code. For example: 57 | 58 | ```bash 59 | text/plain 60 | image/png 61 | image/webp 62 | application/javascript 63 | application/json 64 | ``` 65 | 66 | Each "type" has its own set of "subtypes." All MIME types have a "type" and a "subtype." 67 | 68 | You can add more information with an optional "parameter." This looks like "type/subtype;parameter=value." For example, you can add the "charset" parameter to tell the computer what character set to use for the text. If you don't specify a "charset," the default is ASCII. To specify a UTF-8 text file, the MIME type "text/plain;charset=UTF-8" is used. 69 | 70 | MIME types can be written in uppercase or lowercase, but lowercase is more common. The parameter values can be case-sensitive. 71 | 72 | We won't be diving too much into MIME types just yet, we'll come back to these when we start working on our backend library. 73 | 74 | ### But why the wildcard `*/*`? 75 | 76 | The wildcard **`*/*`** approach is a versatile strategy. It's like telling the server, "I'm flexible. Show me what you've got, and I'll adapt." This can be handy when you're browsing web pages with a mix of images, videos, text, and more. Instead of specifying a narrow set of MIME types, you're leaving room for surprises. 77 | 78 | So, when you see **`> Accept: */*`** in your cURL output or in any request header, remember that it's your browser's (or client's) way of embracing the diversity of the digital marketplace. It's a friendly nod to MIME types, indicating that you're ready to explore whatever content the server has to offer. 79 | 80 | ### The `Content-Type` header 81 | 82 | The `Content-Type` header tells what kind and how the data is sent in the request or response body. It helps the receiver understand and handle the content correctly. The `Content-Type` header can be set on response headers, as well as the request headers. 83 | 84 | > Note: The value of the `Content-Type` header should be a valid MIME type. 85 | 86 | #### `Content-Type` on request header 87 | 88 | When a client sends an HTTP request to a server, the `Content-Type` header can be included to inform the server about the type of data being sent in the request body. For example, if you're submitting a form that includes file uploads, you would specify the `Content-Type` header to match the format of the uploaded file. This helps the server understand how to process the incoming data. 89 | 90 | > We haven't reached the response part of the `cURL` request yet, but for now just bare with me. 91 | 92 | Here's an example of including `Content-Type` in a request header: 93 | 94 | ```bash 95 | POST /accounts HTTP/1.1 96 | Host: github.com 97 | Content-Type: application/json 98 | ``` 99 | 100 | #### `Content-Type` on response header 101 | 102 | In a response by a server, the `Content-Type` header informs the client about the format of the content in the response body. This helps the client, such as a browser, to properly interpret and render the received data. For instance, when a server sends an HTML page to a browser, it specifies the `Content-Type` as `text/html`. 103 | 104 | Here's an example of including `Content-Type` in a response header: 105 | 106 | ```bash 107 | HTTP/1.1 201 CREATED 108 | Content-Type: text/html; charset=UTF-8 109 | Content-Length: 10 110 | ``` 111 | 112 | ### The `charset=UTF-8`: character encoding 113 | 114 | The **`charset`** parameter in the **`Content-Type`** header tells which character encoding is used for text-based content. Character encoding specifies how characters are represented as binary data (bytes). Each character encoding supports different character sets and languages. 115 | 116 | #### Universal Character Encoding 117 | 118 | **`UTF-8`** stands for a character encoding that can represent almost all characters in the Unicode standard. Unicode contains many characters used in different languages and scripts all over the world. 119 | 120 | **Significance in HTML Content:** 121 | 122 | When you use **`charset=UTF-8`** in HTML, it means that the content is using the UTF-8 character encoding. This is important because it makes sure that characters from different languages and scripts will show up correctly in browsers and other apps. 123 | 124 | For example: 125 | 126 | ```html 127 | 128 | 129 | 130 | 131 | UTF-8 Example 132 | 133 | 134 |

Hello, 你好, こんにちは

135 | 136 | 137 | ``` 138 | 139 | In this HTML markup, the **``** tag inside the **``** tag specifies that the document is encoded using UTF-8. This allows the browser to accurately render characters from multiple languages, such as English, Chinese, Tamil, and Japanese, all in the same document. 140 | 141 | **Universal Compatibility:** 142 | 143 | Using UTF-8 as the character encoding ensures universal compatibility, as it can represent characters from various languages without any issues. It is a popular choice for web content due to its versatility and support for a wide range of characters. 144 | 145 | This should be enough for a basic understanding of the `Content-Type` header and the MIME type. We'll start talking about the response part of the `cURL` output in the next chapter. 146 | 147 | [![Read Next](/assets/imgs/next.png)](/chapters/ch05.4-headers.md) 148 | -------------------------------------------------------------------------------- /chapters/ch06.03-improving-the-router-api.md: -------------------------------------------------------------------------------- 1 | [![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.02-the-router-class.md) 2 | 3 | ## Improving the `Router` API 4 | 5 | 6 | 7 | The utility method on the `Router` class - `addRoute` is a bit too verbose. You need to specify the HTTP method as a string. It would get tedious when there are suppose hundreds of API routes in an application. Also, devs might not know whether the HTTP methods should be sent in lower-case or upper-case without looking at the source. 8 | 9 | Let's abstract that functionality away from the developer, making sure the developers only need to worry about the important pieces. 10 | 11 | Current way to add routes: 12 | 13 | ```js 14 | // file: index.js 15 | 16 | class Router { 17 | constructor() { 18 | this.routes = {}; 19 | } 20 | 21 | addRoute(method, path, handler) { 22 | this.routes[`${method} ${path}`] = handler; 23 | } 24 | ... 25 | } 26 | ``` 27 | 28 | Let's add two new methods named `get` and `post`, and add some type checks in the `addRoute` method: 29 | 30 | ```js 31 | // file: index.js 32 | 33 | const HTTP_METHODS = { 34 | GET: "GET", 35 | POST: "POST", 36 | PUT: "PUT", 37 | DELETE: "DELETE", 38 | PATCH: "PATCH", 39 | HEAD: "HEAD", 40 | OPTIONS: "OPTIONS", 41 | CONNECT: "CONNECT", 42 | TRACE: "TRACE", 43 | }; 44 | 45 | class Router { 46 | constructor() { 47 | this.routes = {} 48 | } 49 | 50 | #addRoute(method, path, handler) { 51 | if (typeof path !== "string" || typeof handler !== "function") { 52 | throw new Error("Invalid argument types: path must be a string and handler must be a function"); 53 | } 54 | 55 | this.routes.set(`${method} ${path}`, handler); 56 | } 57 | 58 | get(path, handler) { 59 | this.#addRoute(HTTP_METHODS.GET, path, handler); 60 | } 61 | post(path, handler) { 62 | this.#addRoute(HTTP_METHODS.POST, path, handler); 63 | } 64 | put(path, handler) { 65 | this.#addRoute(HTTP_METHODS.PUT, path, handler); 66 | } 67 | 68 | delete(path, handler) { 69 | this.#addRoute(HTTP_METHODS.DELETE, path, handler); 70 | } 71 | 72 | patch(path, handler) { 73 | this.#addRoute(HTTP_METHODS.PATCH, path, handler); 74 | } 75 | 76 | head(path, handler) { 77 | this.#addRoute(HTTP_METHODS.HEAD, path, handler); 78 | } 79 | 80 | options(path, handler) { 81 | this.#addRoute(HTTP_METHODS.OPTIONS, path, handler); 82 | } 83 | 84 | connect(path, handler) { 85 | this.#addRoute(HTTP_METHODS.CONNECT, path, handler); 86 | } 87 | 88 | trace(path, handler) { 89 | this.#addRoute(HTTP_METHODS.TRACE, path, handler); 90 | } 91 | 92 | ... 93 | } 94 | ``` 95 | 96 | Let's go through the new additions in our code: 97 | 98 | ```js 99 | get(path, handler) { 100 |     this.#addRoute(HTTP_METHODS.GET, path, handler); 101 | } 102 | 103 | post(path, handler) { 104 | this.#addRoute(HTTP_METHODS.POST, path, handler); 105 | } 106 | /** rest HTTP method handlers **/ 107 | ``` 108 | 109 | We've created new utility methods on the `Router` class. Each one of these methods call the `addRoute` method by passing in required parameters. You'd notice that we've also made the `addRoute` method private. Since we wish to use it internally in our library and not expose it, it's a good practice to hide it from any external use. 110 | 111 | ```js 112 | const HTTP_METHODS = { ... } 113 | ``` 114 | 115 | We've created an object of all the HTTP methods, so that we can use their names with the `HTTP_METHODS` namespace, instead of directly passing in strings as an argument, for example: 116 | 117 | ```js 118 | this.#addRoute("GET", path, handler); 119 | ``` 120 | 121 | There's nothing wrong with this approach too, but I prefer to avoid raw strings. `"GET"` can mean many things, but `HTTP_METHODS.GET` gives us the actual idea of what it is all about. 122 | 123 | Let's update our testing code to call the newly created http methods instead: 124 | 125 | ```js 126 | // file: index.js 127 | 128 | ... 129 | 130 | router.get("/", function handleGetBasePath(req, res) { 131 | console.log("Hello from GET /"); 132 | res.end(); 133 | }); 134 | 135 | router.post("/", function handlePostBasePath(req, res) { 136 | console.log("Hello from POST /"); 137 | res.end() 138 | }); 139 | 140 | ... 141 | ``` 142 | 143 | If we do a quick test on both the endpoints, every thing seems to be working alright: 144 | 145 | ```bash 146 | $ curl -X POST http://localhost:5255/ -v 147 | # Success 148 | 149 | $ curl -X POST http://localhost:5255/foo -v 150 | # Not found 151 | 152 | $ curl -X POST http://localhost:5255/foo/bar -v 153 | # Not found 154 | 155 | $ curl http://localhost:5255/ -v 156 | # Success 157 | 158 | $ curl http://localhost:5255/foo -v 159 | # Not found 160 | 161 | $ curl http://localhost:5255/foo -v 162 | # Not found 163 | ``` 164 | 165 | Great! This looks much better than the previous implementation. 166 | 167 | [![Read Next](/assets/imgs/next.png)](/chapters/ch06.04-the-need-for-a-trie.md) 168 | -------------------------------------------------------------------------------- /chapters/ch06.04-the-need-for-a-trie.md: -------------------------------------------------------------------------------- 1 | [![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.03-improving-the-router-api.md) 2 | 3 | ## The Need for a `Trie` 4 | 5 | 6 | 7 | Until now, we've been using a straightforward object to store our routes. While this is easy to understand, it's not the most efficient way to store routes, especially when we have a large number of them or when we introduce dynamic routing capabilities like `/users/:id`. It's a simple and readable approach but lacks efficiency and the capability for dynamic routing. As we aim to build a robust, scalable, and high-performance backend framework, it is crucial to optimize our routing logic. 8 | 9 | As long as you don't need dynamic parameters, or query parameters, you'd be good enough with a javascript object (like we do now), or a `Map`. But a backend framework that doesn't supports dynamic parameters, or query parsing is as good as a social media site without an ability to add friends. 10 | 11 | In this chapter, we'll explore a new data-structure that you may not have heard of before - **Trie**. We'll also look at how we can utilize it to enhance our router's performance. 12 | 13 | For example, imagine we have the following four routes: 14 | 15 | ```bash 16 | GET /api/v1/accounts/friend 17 | GET /api/v1/accounts/stats 18 | GET /api/v1/accounts/upload 19 | GET /api/v1/accounts/blocked_users 20 | POST /api/v1/accounts/friend 21 | POST /api/v1/accounts/stats 22 | POST /api/v1/accounts/upload 23 | POST /api/v1/accounts/blocked_users 24 | ``` 25 | 26 | Our current implementation will have them stored as separate keys in the object: 27 | 28 | ```json 29 | { 30 | "GET /api/v1/accounts/friend": function handle_friend() { ... }, 31 | "GET /api/v1/accounts/stats": function handle_stats() { ... }, 32 | "GET /api/v1/accounts/upload": function handle_upload() { ... }, 33 | "GET /api/v1/accounts/blocked_users": function handle_blocked_users() { ... }, 34 | "POST /api/v1/accounts/friend": function handle_friend() { ... }, 35 | "POST /api/v1/accounts/stats": function handle_stats() { ... }, 36 | "POST /api/v1/accounts/upload": function handle_upload() { ... }, 37 | "POST /api/v1/accounts/blocked_users": function handle_blocked_users() { ... } 38 | } 39 | ``` 40 | 41 | That is not efficient. For most of the applications this is nothing to worry about, but there's a better way. Also with this approach it becomes impossible to extend our router with the other functionalities we mentioned - dynamic routes, queries etc. There's a way to do some regex sorcery to achieve it, but that method will be way way slower. You don't need to sacrifice performance in order to support more features. 42 | 43 | A better way to store the routes could be the following: 44 | 45 | ```json 46 | { 47 | "/api": { 48 | "/v1": { 49 | "/accounts": { 50 | "friend": function handle_friend() { ... }, 51 | "stats": function handle_stats() { ... }, 52 | "upload": function handle_upload() { ... }, 53 | "blocked_users": function handle_blocked_users() { ... } 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | This is an easy way to think of how a `Trie` stores the paths. 61 | 62 | ### What is a `Trie` anyway? 63 | 64 | A `Trie`, which is also known as a prefix tree, is a specialized tree structure used for storing a mapping between keys and values, where the keys are generally strings. This structure is organized in such a way that all the child nodes that stem from a single parent node have a shared initial sequence of characters, or a "common prefix." So the position of a node in the Trie dictates what key it corresponds to, rather than storing the key explicitly in the node itself. 65 | 66 | Imagine we have the following routes: 67 | 68 | ```bash 69 | 'GET /users' 70 | 'GET /users/id' 71 | 'POST /users' 72 | ``` 73 | 74 | With our current implementation, the routes object would look like: 75 | 76 | ```json 77 | { 78 | "GET /users": handler, 79 | "GET /users/id": handler, 80 | "POST /users": handler 81 | } 82 | ``` 83 | 84 | But, with a Trie, it will look like the following: 85 | 86 | ```bash 87 | [root] 88 | | 89 | GET 90 | | 91 | users 92 | / \ 93 | POST GET 94 | \ 95 | id 96 | ``` 97 | 98 | Every node, including `root` will be an object that contain some necessary information with it. 99 | 100 | 1. `handler`: The function to be executed when the route represented by the path to this node is accessed. Not all nodes will have handlers, only the nodes that correspond to complete routes. 101 | 102 | 2. `path`: The current route segment in string, for example - `/users` or `/id` 103 | 104 | 3. `param` and `paramName`: If the current path is `/:id` and the client makes a request at `/xyz`, the `param` will be `xyz` and the `paramName` will be `id`. 105 | 106 | 4. `children`: Any children nodes. (We'll get more deep into this in the upcoming chapters) 107 | 108 | Enough with the theory. In the next chapter, we'll dive into our very first exercise for this book: **implementing a Trie**. 109 | 110 | [![Read Next](/assets/imgs/next.png)](/chapters/ch06.05-ex-implementing-a-trie.md) 111 | -------------------------------------------------------------------------------- /chapters/ch06.07-ex-adding-http-methods.md: -------------------------------------------------------------------------------- 1 | ## Ex. Adding `HTTP` method support 2 | 3 | 4 | 5 | So far, we've built a router capable of matching URL paths to specific handlers. This is a good starting point, but as of now, our router does not differentiate between different HTTP methods like GET, POST, PUT, DELETE, etc. In real-world applications, the same URL path can behave differently based on the HTTP method used, making our current router almost useless for such scenarios. 6 | 7 | ### Requirements 8 | 9 | To make our router more useful and versatile, we need to extend the existing `TrieRouter` and `RouteNode` classes to support different HTTP methods (GET, POST, PUT, DELETE, etc.). This means that each node in the Trie could potentially have multiple handler functions, one for each HTTP method. 10 | 11 | ### More details 12 | 13 | 1. Continue with the existing router class `TrieRouter`. Add new functionalities to it. 14 | 15 | 2. The key in the `handler` `Map` will be the HTTP method as a string (like "GET", "POST") and the value will be the handler function for that HTTP method. 16 | 17 | 3. Modify the `addRoute` method of the `TrieRouter` class to take an additional parameter `method`. 18 | 19 | - `method`: A string representing the HTTP method. This could be "GET", "POST", "PUT", "DELETE", etc. 20 | 21 | 4. Also update the `findRoute` method. Now it will have another parameter - `method`, to search for routes based on the HTTP method as well as the path. 22 | 23 | 5. If a handler for a specific path and HTTP method is already present, the new handler should override the old one. 24 | 25 | ### Example 26 | 27 | Once implemented, the usage might look like this: 28 | 29 | ```js 30 | const trieRouter = new TrieRouter(); 31 | 32 | function getHandler() {} 33 | function postHandler() {} 34 | 35 | trieRouter.addRoute("/home", "GET", getHandler); 36 | trieRouter.addRoute("/home", "POST", postHandler); 37 | 38 | console.log(trieRouter.findRoute("/home", "GET")); // -> fn getHandler() {..} 39 | console.log(trieRouter.findRoute("/home", "PATCH")); // -> null or undefined 40 | console.log(trieRouter.findRoute("/home", "POST")); // -> fn postHanlder() {..} 41 | ``` 42 | 43 | Go ahead and add the functionality to our `TrieRouter` class. This will involve making a lot of changes to the previous code. Feel free to share your implementation or ask for feedback in the [Github discussions](https://github.com/ishtms/learn-nodejs-hard-way/discussions) section. 44 | 45 | ### Hints 46 | 47 | 1. When you're adding or searching for a route, make sure to consider both the path and the HTTP method. 48 | 49 | 2. Take care to handle the HTTP method case-insensitively (prefer uppercase). It's common to receive HTTP method names in different cases. 50 | 51 | 3. Be careful with your error-handling logic to correctly manage the situation where the client does not provide a valid HTTP method. 52 | 53 | 4. As with Challenge 1, start by making sure the Trie works for a simple case before diving into the more complex functionalities. 54 | 55 | 5. Don't forget to update your utility functions and other methods to be compatible with these new requirements. 56 | 57 | ### Solution 58 | 59 | Here's the solution I came up with: 60 | 61 | ```js 62 | const HTTP_METHODS = { 63 | GET: "GET", 64 | POST: "POST", 65 | PUT: "PUT", 66 | DELETE: "DELETE", 67 | PATCH: "PATCH", 68 | HEAD: "HEAD", 69 | OPTIONS: "OPTIONS", 70 | CONNECT: "CONNECT", 71 | TRACE: "TRACE", 72 | }; 73 | 74 | class RouteNode { 75 | constructor() { 76 | this.children = new Map(); 77 | this.handler = new Map(); 78 | } 79 | } 80 | 81 | class TrieRouter { 82 | constructor() { 83 | this.root = new RouteNode(); 84 | } 85 | 86 | addRoute(path, method, handler) { 87 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 88 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 89 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 90 | 91 | let currentNode = this.root; 92 | let routeParts = path.split("/").filter(Boolean); 93 | 94 | for (let idx = 0; idx < routeParts.length; idx++) { 95 | const segment = routeParts[idx].toLowerCase(); 96 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 97 | 98 | let childNode = currentNode.children.get(segment); 99 | if (!childNode) { 100 | childNode = new RouteNode(); 101 | currentNode.children.set(segment, childNode); 102 | } 103 | 104 | currentNode = childNode; 105 | } 106 | currentNode.handler.set(method, handler); // Changed this line 107 | } 108 | 109 | findRoute(path, method) { 110 | let segments = path.split("/").filter(Boolean); 111 | let currentNode = this.root; 112 | 113 | for (let idx = 0; idx < segments.length; idx++) { 114 | const segment = segments[idx]; 115 | 116 | let childNode = currentNode.children.get(segment); 117 | if (childNode) { 118 | currentNode = childNode; 119 | } else { 120 | return null; 121 | } 122 | } 123 | 124 | return currentNode.handler.get(method); // Changed this line 125 | } 126 | 127 | printTree(node = this.root, indentation = 0) { 128 | /** Unchanged **/ 129 | } 130 | } 131 | ``` 132 | 133 | The new HTTP method implementation introduces only some minor key changes to extend the existing router implementation to support HTTP methods. Below are the details of what was changed and why: 134 | 135 | ```js 136 | const HTTP_METHODS = { 137 | GET: "GET", 138 | POST: "POST", 139 | PUT: "PUT", 140 | DELETE: "DELETE", 141 | PATCH: "PATCH", 142 | HEAD: "HEAD", 143 | OPTIONS: "OPTIONS", 144 | CONNECT: "CONNECT", 145 | TRACE: "TRACE", 146 | }; 147 | ``` 148 | 149 | Firstly, we've defined a constant object named `HTTP_METHODS` to represent the different HTTP methods. This serves as a reference for the HTTP methods that our `TrieRouter` class will support. We might even do some validation, but that is not necessary (we'll look at it in a later chapter why validation isn't required here) 150 | 151 | ```js 152 | class TrieRouter { 153 | addRoute(path, method, handler) { ... } 154 | ... 155 | } 156 | ``` 157 | 158 | In our `TrieRouter` class, we updated the `addRoute` method. It now takes an additional argument, `method`, which specifies the HTTP method for the route. 159 | 160 | ```js 161 | addRoute(path, method, handler) { 162 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 163 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 164 | 165 | // New check for HTTP method 166 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 167 | ... 168 | } 169 | ``` 170 | 171 | The error handling has been updated to ensure the `method` is a valid HTTP method. 172 | 173 | ```js 174 | this.handler = new Map(); 175 | ``` 176 | 177 | The `handler` in `RouteNode` has changed from a single function reference to a `Map`. This allows you to store multiple handlers for the same path but with different HTTP methods. 178 | 179 | ```js 180 | addRoute(path, method, handler) { 181 | ... 182 | // Previous -> currentNode.handler = handler; 183 | currentNode.handler.set(method, handler); 184 | } 185 | 186 | findRoute(path, method) { 187 | ... 188 | // Previous -> return currentNode.handler; 189 | return currentNode.handler.get(method); // Changed this line 190 | } 191 | ``` 192 | 193 | In both the `addRoute` and `findRoute` methods, we've updated the line that sets and gets the handler for a specific path. Now, the handler is stored in the `handler` map of the current node, with the HTTP method as the key. 194 | 195 | [![Read Next](/assets/imgs/next.png)](/chapters/ch06.09-ex-dynamic-routing.md) 196 | -------------------------------------------------------------------------------- /chapters/ch06.08-adding-verbs-api.md: -------------------------------------------------------------------------------- 1 | ## Adding HTTP methods to the Router 2 | 3 | 4 | 5 | In this exercise, we will improve our `TrieRouter` API by implementing support for HTTP verbs (GET, POST, PUT, DELETE, etc.) directly instead of using raw strings. Let's look at our current implementation of the `TrieRouter`: 6 | 7 | ```js 8 | trieRouter.addRoute("GET", "/users", () => {}); 9 | trieRouter.addRoute("GET", "/", () => {}); 10 | ``` 11 | 12 | As you could already feel, this approach is not very flexible and uses raw strings, which can lead to typing errors, and has no auto-completion support unfortunately. Let's improve this by adding support for HTTP methods directly to the `TrieRouter` API. 13 | 14 | ### Update the `TrieRouter` class 15 | 16 | ```js 17 | const HTTP_METHODS = { ... }; 18 | 19 | class TrieRouter { 20 | ... 21 | 22 | #addRoute(path, method, handler) { 23 | ... 24 | } 25 | 26 | get(path, handler) { 27 | this.#addRoute(path, HTTP_METHODS.GET, handler); 28 | } 29 | 30 | post(path, handler) { 31 | this.#addRoute(path, HTTP_METHODS.POST, handler); 32 | } 33 | 34 | put(path, handler) { 35 | this.#addRoute(path, HTTP_METHODS.PUT, handler); 36 | } 37 | 38 | delete(path, handler) { 39 | this.#addRoute(path, HTTP_METHODS.DELETE, handler); 40 | } 41 | 42 | patch(path, handler) { 43 | this.#addRoute(path, HTTP_METHODS.PATCH, handler); 44 | } 45 | 46 | head(path, handler) { 47 | this.#addRoute(path, HTTP_METHODS.HEAD, handler); 48 | } 49 | 50 | options(path, handler) { 51 | this.#addRoute(path, HTTP_METHODS.OPTIONS, handler); 52 | } 53 | 54 | connect(path, handler) { 55 | this.#addRoute(path, HTTP_METHODS.CONNECT, handler); 56 | } 57 | 58 | trace(path, handler) { 59 | this.#addRoute(path, HTTP_METHODS.TRACE, handler); 60 | } 61 | ... 62 | } 63 | ``` 64 | 65 | #### Explanation 66 | 67 | Firstly, we've added dedicated methods for each HTTP method in the `TrieRouter` class. This allows users to define routes more intuitively using method-specific calls like `trieRouter.get('/home', handler)` for the GET method and `trieRouter.post('/home', handler)` for the POST method. 68 | 69 | In each of these methods, we call the existing `addRoute` method, passing the appropriate HTTP method from the `HTTP_METHODS` object. 70 | 71 | This change allows for a consistent and clear way to find routes based on the HTTP method. 72 | 73 | Sedcondly, we've made the `addRoute` method private by prefixing it with a `#`. This means that the `#addRoute` method can now only be accessed from within the `TrieRouter` class and not from outside. 74 | 75 | Now, to test the new API, let's update our previous example: 76 | 77 | ```js 78 | const trieRouter = new TrieRouter(); 79 | 80 | trieRouter.get("/users", function get1() {}); 81 | trieRouter.post("/users", function post1() {}); 82 | trieRouter.put("/users", function put1() {}); 83 | trieRouter.delete("/users", function delete1() {}); 84 | 85 | console.log(trieRouter.findRoute("/users/e", HTTP_METHODS.GET)); // null 86 | console.log(trieRouter.findRoute("/users", HTTP_METHODS.POST)); // function post1() {} 87 | console.log(trieRouter.findRoute("/users", HTTP_METHODS.PUT)); // function put1() {} 88 | console.log(trieRouter.findRoute("/users", HTTP_METHODS.TRACE)); // undefined 89 | ``` 90 | 91 | Looks good, and now we have a more intuitive way to define routes based on HTTP methods. Let's move on to the next exercise to add support for route parameters. 92 | -------------------------------------------------------------------------------- /chapters/ch06.10-running-our-server.md: -------------------------------------------------------------------------------- 1 | ## Running Our Server 2 | 3 | 4 | 5 | We have a working implementation of our `TrieRouter` class, which has enough functionality to handle routing for our server. But, we don't have a way to run our server or listen for requests yet. 6 | 7 | In this chapter, we will implement a `run` function that accepts a router (`TrieRouter`) and a port (`number`) argument. This function will serve as the entry point of our server code, allowing us to handle incoming requests and route them to the appropriate handlers defined in our router. 8 | 9 | But before that, let's refactor our code a little bit. We're going to rename the `TrieRouter` class to `Router`. Secondly, we're going to add JSDoc comments to all the methods in the `Router` class, to make our lives easier with the auto-completion and documentation. 10 | 11 | ### Refactoring the `TrieRouter` class 12 | 13 | ```js 14 | const HTTP_METHODS = { ... } // Remains unchanged 15 | 16 | class RouteNode { 17 | constructor() { 18 | /** @type {Map} */ 19 | this.children = new Map(); 20 | 21 | /** @type {Map} */ 22 | this.handler = new Map(); 23 | 24 | /** @type {Array} */ 25 | this.params = []; 26 | } 27 | } 28 | 29 | 30 | class Router { 31 | constructor() { 32 | /** @type {RouteNode} */ 33 | this.root = new RouteNode(); 34 | } 35 | 36 | /** 37 | * @param {String} path 38 | * @param { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } method 39 | * @param {Function} handler 40 | */ 41 | #verifyParams(path, method, handler) { ... } 42 | 43 | 44 | /** 45 | * @param {String} path 46 | * @param { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } method 47 | * @param {Function} handler 48 | */ 49 | #addRoute(path, method, handler) { ... } 50 | 51 | /** 52 | * @param {String} path 53 | * @param { 'GET' | 'POST' | 'PUT ' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT ' | 'TRACE ' } method 54 | * @returns { { params: Object, handler: Function } | null } 55 | */ 56 | findRoute(path, method) { ... } 57 | 58 | /** 59 | * @param {String} path 60 | * @param {Function} handler 61 | */ 62 | get(path, handler) { 63 | this.#addRoute(path, HTTP_METHODS.GET, handler); 64 | } 65 | 66 | /** For POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE, we are going to re-use the same JSDoc comment as `get` method */ 67 | 68 | /** 69 | * @param {RouteNode} node 70 | * @param {number} indentation 71 | */ 72 | printTree(node = this.root, indentation = 0) { ... } 73 | } 74 | ``` 75 | 76 | As you can see, we're repeating this JSDoc comment for a couple of methods: 77 | 78 | ```js 79 | /** 80 | * @param {String} path 81 | * @param { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } method 82 | * @param {Function} handler 83 | */ 84 | ``` 85 | 86 | This is not a good practice. Instead we can make use of something called **Type Aliases**. 87 | 88 | Type aliases are a way to give a type a name, just like a variable. Each type **should be** distinct. They are exactly the same as the original type, but with a different name. This is useful when you want to refer to the same type multiple times and don't want to repeat the same type definition. 89 | 90 | ### Type Aliases 91 | 92 | Let's add a type alias for the HTTP methods: 93 | 94 | ```js 95 | /** 96 | * @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod 97 | */ 98 | ``` 99 | 100 | And, to use this type alias in our JSDoc comments: 101 | 102 | ```js 103 | /** 104 | * @param {String} path 105 | * @param {HttpMethod} method 106 | * @param {Function} handler 107 | */ 108 | #verifyParams(path, method, handler) { ... } 109 | 110 | /** 111 | * @param {String} path 112 | * @param {HttpMethod } method 113 | * @param {Function} handler 114 | */ 115 | #addRoute(path, method, handler) { ... } 116 | 117 | /** And so on... */ 118 | ``` 119 | 120 | But there's a small issue. We have defined the `HttpMethod` type alias in the same file as the `Router` class. This will make it impossible to use the `HttpMethod` type alias in other files. To fix this, we can create a new file `globals.js` and move all the global type aliases here. 121 | 122 | ```js 123 | // file: globals.js 124 | 125 | /** 126 | * @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod 127 | */ 128 | ``` 129 | 130 | Now you'd be able to use the `HttpMethod` type alias in any file. That's enough refactoring for now. 131 | 132 | ### The `run` function 133 | 134 | ```js 135 | const { createServer } = require("node:http"); 136 | 137 | /** 138 | * Run the server on the specified port 139 | * @param {Router} router - The router to use as the main request handler 140 | * @param {number} port - The port to listen on 141 | */ 142 | function run(router, port) { 143 | if (!(router instanceof Router)) { 144 | throw new Error("`router` argument must be an instance of Router"); 145 | } 146 | 147 | if (typeof port !== "number") { 148 | throw new Error("`port` argument must be a number"); 149 | } 150 | 151 | createServer(function _create(req, res) { 152 | const route = router.findRoute(req.url, req.path); 153 | 154 | if (route?.handler) { 155 | req.params = route.params; 156 | route.handler(req, res); 157 | } else { 158 | res.writeHead(404, null, { "content-length": 9 }); 159 | res.end("Not Found"); 160 | } 161 | }).listen(port); 162 | } 163 | ``` 164 | 165 | Let's go through the code line by line: 166 | 167 | ```js 168 | const { createServer } = require("node:http"); 169 | ``` 170 | 171 | We're importing the `createServer` function that the `node:http` module provides. This function is used to create an HTTP server that listens for requests on a specified `port`. We already created a demo server using `http.createServer()` in [HTTP Deep Dive](https://github.com/ishtms/learn-nodejs-hard-way/blob/master/chapters/ch05.0-http-deep-dive.md). 172 | 173 | ```js 174 | function run(router, port) { ... } 175 | ``` 176 | 177 | This is going to be the main entry point of our library. The `run` function is accepts two arguments: `router` and `port`. The `router` will be an instance of the `Router` class that we defined earlier, and the `port` will be the port number on which the server will listen for incoming requests. 178 | 179 | ```js 180 | if (!(router instanceof Router)) { 181 | throw new Error("`router` argument must be an instance of Router"); 182 | } 183 | 184 | if (typeof port !== "number") { 185 | throw new Error("`port` argument must be a number"); 186 | } 187 | ``` 188 | 189 | Some basic type checking to ensure that the `router` argument is an instance of the `Router` class and the `port` argument is a number. 190 | 191 | ```js 192 | createServer(function _create(req, res) { ... }).listen(port); 193 | ``` 194 | 195 | We're creating an HTTP server using the `createServer` function. To re-iterate, the `createServer` function takes a single argument\*, which is a callback function. The callback function will receive two arguments: `req` (the `Http.IncomingMessage` object) and `res` (the `Http.ServerResponse` object). 196 | 197 | ```js 198 | const route = router.findRoute(req.url, req.path); 199 | 200 | if (route?.handler) { 201 | req.params = route.params; 202 | route.handler(req, res); 203 | } else { 204 | res.writeHead(404, null, { "Content-Length": 9 }); 205 | res.end("Not Found"); 206 | } 207 | ``` 208 | 209 | We're calling the `findRoute` method on the `router` object to find the route that matches the incoming request. The `findRoute` method will return an object with two properties: `handler` and `params`. If a route is found, we'll call the `handler` function with the `req` and `res` objects. If no route is found, we'll return a `404 Not Found` response. 210 | 211 | Inside the `if` statement, we're attaching a new property `req.params` to the `req` object. This property will contain the parameters extracted from the URL. The client can easily access the parameters using `req.params`. 212 | 213 | You might have noticed that we're using a hard-coded `Content-Length` header with a value of `9`. This is because, if we do not specify the `Content-Length` header, the response headers will include a header `Transfer-Encoding: chunked`, which has a performance impact. We discussed about this in a previous chapter - [Chunks, oh no!](chapters/ch06.01-basic-router-implementation.md#chunks-oh-no-) 214 | 215 | That's it! We have implemented the `run` function, which will allow us to run our server and listen for incoming requests. In the next chapter, we'll implement a simple server using our `Router` class and the `run` function. 216 | 217 | > \*The callback function has multiple overloads, i.e it has a couple more function signatures. But for now, we're only interested in the one that takes a single callback function. 218 | -------------------------------------------------------------------------------- /genIndex.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const readline = require("readline"); 4 | 5 | const rootDir = __dirname; 6 | const chaptersDir = path.join(rootDir, "chapters"); 7 | const readmeTemplatePath = path.join(rootDir, "Readme.tl"); 8 | const readmeOutputPath = path.join(rootDir, "Readme.md"); 9 | 10 | async function generateTOC() { 11 | const files = fs.readdirSync(chaptersDir).filter((file) => file.endsWith(".md") && file !== "Readme.md"); 12 | let toc = "# Table of contents\n\n"; 13 | 14 | for (const file of files) { 15 | const filePath = path.join(chaptersDir, file); 16 | const headings = await extractHeadings(filePath); 17 | const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/"); 18 | toc += formatHeadings(headings, relativePath); 19 | } 20 | 21 | const templateContent = fs.readFileSync(readmeTemplatePath, "utf-8"); 22 | const outputContent = templateContent.replace("{{toc}}", toc); 23 | fs.writeFileSync(readmeOutputPath, outputContent); 24 | 25 | console.log("Table of contents generated successfully!"); 26 | } 27 | 28 | async function extractHeadings(filePath) { 29 | const fileStream = fs.createReadStream(filePath); 30 | const rl = readline.createInterface({ 31 | input: fileStream, 32 | crlfDelay: Infinity, 33 | }); 34 | 35 | const headings = []; 36 | let insideCodeBlock = false; 37 | 38 | for await (const line of rl) { 39 | if (line.trim().startsWith("```")) { 40 | insideCodeBlock = !insideCodeBlock; 41 | } 42 | 43 | if (!insideCodeBlock) { 44 | const match = line.match(/^(#{1,3})\s+(.*)/); 45 | if (match) { 46 | const level = match[1].length; 47 | const text = match[2]; 48 | const anchor = text.toLowerCase().replace(/[^\w]+/g, "-"); 49 | headings.push({ level, text, anchor }); 50 | } 51 | } 52 | } 53 | 54 | return headings; 55 | } 56 | 57 | function formatHeadings(headings, relativePath) { 58 | let formatted = ""; 59 | for (const heading of headings) { 60 | const indent = " ".repeat(heading.level - 1); 61 | const link = `${relativePath}#${heading.anchor}`; 62 | formatted += `${indent}- [${heading.text}](${link})\n`; 63 | } 64 | return formatted; 65 | } 66 | 67 | generateTOC().catch((err) => console.error(err)); 68 | -------------------------------------------------------------------------------- /new-in-this-release.log: -------------------------------------------------------------------------------- 1 | 8238a34 adds chapter 06.11 - Buildling a web server 2 | 41b1f62 updates `genIndex.js` to limit the readme depth to 3 3 | 6bcda98 updates chapter naming if the sub-chapters are more than 10 for maintaining lexicographical ordering on github and generated files 4 | 665c8a1 adds chapter 06.10 - running-our-server 5 | 9816cbb adds chapter 06.10 - running-our-server 6 | e92e098 fixes #57 7 | 7ca7243 fixes #57 8 | 7718076 Update Readme.md 9 | bb43d1c adds automation 10 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-nodejs-hard-way", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "learn-nodejs-hard-way", 9 | "version": "1.0.0", 10 | "license": "CC-BY-NC-ND-4.0", 11 | "devDependencies": { 12 | "husky": "^9.1.5" 13 | } 14 | }, 15 | "node_modules/husky": { 16 | "version": "9.1.5", 17 | "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.5.tgz", 18 | "integrity": "sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==", 19 | "dev": true, 20 | "license": "MIT", 21 | "bin": { 22 | "husky": "bin.js" 23 | }, 24 | "engines": { 25 | "node": ">=18" 26 | }, 27 | "funding": { 28 | "url": "https://github.com/sponsors/typicode" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-nodejs-hard-way", 3 | "version": "1.0.0", 4 | "description": "Learn and master NodeJS and backend development by creating a backend framework with 0 dependencies.", 5 | "main": "genIndex.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prepare": "husky" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ishtms/learn-nodejs-hard-way.git" 13 | }, 14 | "keywords": [ 15 | "nodejs", 16 | "javascript", 17 | "api", 18 | "learning", 19 | "node", 20 | "programming", 21 | "server", 22 | "backend", 23 | "book" 24 | ], 25 | "author": "Ishtmeet Singh ", 26 | "license": "CC-BY-NC-ND-4.0", 27 | "bugs": { 28 | "url": "https://github.com/ishtms/learn-nodejs-hard-way/issues" 29 | }, 30 | "homepage": "https://github.com/ishtms/learn-nodejs-hard-way#readme", 31 | "devDependencies": { 32 | "husky": "^9.1.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## Code for all the chapters 2 | -------------------------------------------------------------------------------- /src/chapter_04.0/README.md: -------------------------------------------------------------------------------- 1 | ## Code for the Chapter 04.0 - `logtar` our own logging library 2 | -------------------------------------------------------------------------------- /src/chapter_04.1/README.md: -------------------------------------------------------------------------------- 1 | ## Code for the Chapter 04.1 - Refactoring the code 2 | -------------------------------------------------------------------------------- /src/chapter_04.1/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/logtar') -------------------------------------------------------------------------------- /src/chapter_04.1/lib/config/log-config.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs"); 2 | 3 | const { LogLevel } = require("../utils/log-level"); 4 | const { RollingConfig } = require("./rolling-config"); 5 | 6 | class LogConfig { 7 | /** 8 | * @type {LogLevel} 9 | * @private 10 | * @description The log level to be used. 11 | */ 12 | #level = LogLevel.Info; 13 | 14 | /** 15 | * @type {RollingConfig} 16 | * @private 17 | */ 18 | #rolling_config; 19 | 20 | /** 21 | * @type {string} 22 | * @private 23 | * @description The prefix to be used for the log file name. 24 | * 25 | * If the file prefix is `MyFilePrefix_` the log files created will have the name 26 | * `MyFilePrefix_2021-09-01.log`, `MyFilePrefix_2021-09-02.log` and so on. 27 | */ 28 | #file_prefix = "Logtar_"; 29 | 30 | constructor() { 31 | this.#rolling_config = RollingConfig.with_defaults(); 32 | } 33 | 34 | /** 35 | * @returns {LogConfig} A new instance of LogConfig with default values. 36 | */ 37 | static with_defaults() { 38 | return new LogConfig(); 39 | } 40 | 41 | /** 42 | * @param {string} file_path The path to the config file. 43 | * @returns {LogConfig} A new instance of LogConfig with values from the config file. 44 | * @throws {Error} If the file_path is not a string. 45 | */ 46 | static from_file(file_path) { 47 | const file_contents = fs.readFileSync(file_path); 48 | return LogConfig.from_json(JSON.parse(file_contents)); 49 | } 50 | 51 | /** 52 | * @param {Object} json The json object to be parsed into {LogConfig}. 53 | * @returns {LogConfig} A new instance of LogConfig with values from the json object. 54 | */ 55 | static from_json(json) { 56 | let log_config = new LogConfig(); 57 | Object.keys(json).forEach((key) => { 58 | switch (key) { 59 | case "level": 60 | log_config = log_config.with_log_level(json[key]); 61 | break; 62 | case "rolling_config": 63 | log_config = log_config.with_rolling_config(json[key]); 64 | break; 65 | case "file_prefix": 66 | log_config = log_config.with_file_prefix(json[key]); 67 | break; 68 | } 69 | }); 70 | return log_config; 71 | } 72 | 73 | /** 74 | * @param {LogConfig} log_config The log config to be validated. 75 | * @throws {Error} If the log_config is not an instance of LogConfig. 76 | */ 77 | static assert(log_config) { 78 | if (arguments.length > 0 && !(log_config instanceof LogConfig)) { 79 | throw new Error( 80 | `log_config must be an instance of LogConfig. Unsupported param ${JSON.stringify(log_config)}` 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * @returns {LogLevel} The current log level. 87 | */ 88 | get level() { 89 | return this.#level; 90 | } 91 | 92 | /** 93 | * @param {LogLevel} log_level The log level to be set. 94 | * @returns {LogConfig} The current instance of LogConfig. 95 | * @throws {Error} If the log_level is not an instance of LogLevel. 96 | */ 97 | with_log_level(log_level) { 98 | LogLevel.assert(log_level); 99 | this.#level = log_level; 100 | return this; 101 | } 102 | 103 | /** 104 | * @returns {RollingConfig} The current rolling config. 105 | */ 106 | get rolling_config() { 107 | return this.#rolling_config; 108 | } 109 | 110 | /** 111 | * @param {RollingConfig} config The rolling config to be set. 112 | * @returns {LogConfig} The current instance of LogConfig. 113 | * @throws {Error} If the config is not an instance of RollingConfig. 114 | */ 115 | with_rolling_config(config) { 116 | this.#rolling_config = RollingConfig.from_json(config); 117 | return this; 118 | } 119 | 120 | /** 121 | * @returns {String} The current max file size. 122 | */ 123 | get file_prefix() { 124 | return this.#file_prefix; 125 | } 126 | 127 | /** 128 | * @param {string} file_prefix The file prefix to be set. 129 | * @returns {LogConfig} The current instance of LogConfig. 130 | * @throws {Error} If the file_prefix is not a string. 131 | */ 132 | with_file_prefix(file_prefix) { 133 | if (typeof file_prefix !== "string") { 134 | throw new Error(`file_prefix must be a string. Unsupported param ${JSON.stringify(file_prefix)}`); 135 | } 136 | 137 | this.#file_prefix = file_prefix; 138 | return this; 139 | } 140 | } 141 | 142 | module.exports = { LogConfig }; 143 | -------------------------------------------------------------------------------- /src/chapter_04.1/lib/config/rolling-config.js: -------------------------------------------------------------------------------- 1 | const { RollingTimeOptions, RollingSizeOptions } = require("../utils/rolling-options"); 2 | 3 | class RollingConfig { 4 | /** 5 | * Roll/Create new file every time the current file size exceeds this threshold in `seconds`. 6 | * 7 | * @type {RollingTimeOptions} 8 | * @private 9 | * 10 | */ 11 | #time_threshold = RollingTimeOptions.Hourly; 12 | 13 | /** 14 | * @type {RollingSizeOptions} 15 | * @private 16 | */ 17 | #size_threshold = RollingSizeOptions.FiveMB; 18 | 19 | /** 20 | * @returns {RollingConfig} A new instance of RollingConfig with default values. 21 | */ 22 | static with_defaults() { 23 | return new RollingConfig(); 24 | } 25 | 26 | /** 27 | * @param {number} size_threshold Roll/Create new file every time the current file size exceeds this threshold. 28 | * @returns {RollingConfig} The current instance of RollingConfig. 29 | */ 30 | with_size_threshold(size_threshold) { 31 | RollingSizeOptions.assert(size_threshold); 32 | this.#size_threshold = size_threshold; 33 | return this; 34 | } 35 | 36 | /** 37 | * @param {time_threshold} time_threshold Roll/Create new file every time the current file size exceeds this threshold. 38 | * @returns {RollingConfig} The current instance of RollingConfig. 39 | * @throws {Error} If the time_threshold is not an instance of RollingTimeOptions. 40 | */ 41 | with_time_threshold(time_threshold) { 42 | RollingTimeOptions.assert(time_threshold); 43 | this.#time_threshold = time_threshold; 44 | return this; 45 | } 46 | 47 | /** 48 | * @param {Object} json The json object to be parsed into {RollingConfig}. 49 | * @returns {RollingConfig} A new instance of RollingConfig with values from the json object. 50 | * @throws {Error} If the json is not an object. 51 | */ 52 | static from_json(json) { 53 | let rolling_config = new RollingConfig(); 54 | 55 | Object.keys(json).forEach((key) => { 56 | switch (key) { 57 | case "size_threshold": 58 | rolling_config = rolling_config.with_size_threshold(json[key]); 59 | break; 60 | case "time_threshold": 61 | rolling_config = rolling_config.with_time_threshold(json[key]); 62 | break; 63 | } 64 | }); 65 | 66 | return rolling_config; 67 | } 68 | } 69 | 70 | module.exports = { RollingConfig }; 71 | -------------------------------------------------------------------------------- /src/chapter_04.1/lib/logger.js: -------------------------------------------------------------------------------- 1 | const { LogConfig } = require("./config/log-config"); 2 | const { LogLevel } = require("./utils/log-level"); 3 | 4 | class Logger { 5 | /** 6 | * @type {LogConfig} 7 | */ 8 | #config; 9 | 10 | /** 11 | * @returns {Logger} A new instance of Logger with default config. 12 | */ 13 | static with_defaults() { 14 | return new Logger(); 15 | } 16 | 17 | /** 18 | * 19 | * @param {LogConfig} log_config 20 | * @returns {Logger} A new instance of Logger with the given config. 21 | */ 22 | static with_config(log_config) { 23 | return new Logger(log_config); 24 | } 25 | 26 | /** 27 | * @param {LogConfig} log_config 28 | */ 29 | constructor(log_config) { 30 | log_config = log_config || LogConfig.with_defaults(); 31 | LogConfig.assert(log_config); 32 | this.#config = log_config; 33 | } 34 | 35 | /** 36 | * @returns {LogLevel} The current log level. 37 | */ 38 | get level() { 39 | return this.#config.level; 40 | } 41 | } 42 | 43 | module.exports = { Logger }; -------------------------------------------------------------------------------- /src/chapter_04.1/lib/logtar.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Logger: require('./logger').Logger, 3 | LogConfig: require('./config/log-config').LogConfig, 4 | RollingConfig: require('./config/rolling-config').RollingConfig, 5 | LogLevel: require('./utils/log-level').LogLevel, 6 | RollingTimeOptions: require('./utils/rolling-options').RollingTimeOptions, 7 | RollingSizeOptions: require('./utils/rolling-options').RollingSizeOptions, 8 | }; -------------------------------------------------------------------------------- /src/chapter_04.1/lib/utils/log-level.js: -------------------------------------------------------------------------------- 1 | class LogLevel { 2 | static #Debug = 0; 3 | static #Info = 1; 4 | static #Warn = 2; 5 | static #Error = 3; 6 | static #Critical = 4; 7 | 8 | static get Debug() { 9 | return this.#Debug; 10 | } 11 | 12 | static get Info() { 13 | return this.#Info; 14 | } 15 | 16 | static get Warn() { 17 | return this.#Warn; 18 | } 19 | 20 | static get Error() { 21 | return this.#Error; 22 | } 23 | 24 | static get Critical() { 25 | return this.#Critical; 26 | } 27 | 28 | static assert(log_level) { 29 | if (![this.Debug, this.Info, this.Warn, this.Error, this.Critical].includes(log_level)) { 30 | throw new Error( 31 | `log_level must be an instance of LogLevel. Unsupported param ${JSON.stringify(log_level)}` 32 | ); 33 | } 34 | } 35 | } 36 | 37 | module.exports = { LogLevel }; 38 | -------------------------------------------------------------------------------- /src/chapter_04.1/lib/utils/rolling-options.js: -------------------------------------------------------------------------------- 1 | class RollingSizeOptions { 2 | static OneKB = 1024; 3 | static FiveKB = 5 * 1024; 4 | static TenKB = 10 * 1024; 5 | static TwentyKB = 20 * 1024; 6 | static FiftyKB = 50 * 1024; 7 | static HundredKB = 100 * 1024; 8 | 9 | static HalfMB = 512 * 1024; 10 | static OneMB = 1024 * 1024; 11 | static FiveMB = 5 * 1024 * 1024; 12 | static TenMB = 10 * 1024 * 1024; 13 | static TwentyMB = 20 * 1024 * 1024; 14 | static FiftyMB = 50 * 1024 * 1024; 15 | static HundredMB = 100 * 1024 * 1024; 16 | 17 | static assert(size_threshold) { 18 | if (typeof size_threshold !== "number" || size_threshold < RollingSizeOptions.OneKB) { 19 | throw new Error( 20 | `size_threshold must be at-least 1 KB. Unsupported param ${JSON.stringify(size_threshold)}` 21 | ); 22 | } 23 | } 24 | } 25 | 26 | class RollingTimeOptions { 27 | static Minutely = 60; // Every 60 seconds 28 | static Hourly = 60 * this.Minutely; 29 | static Daily = 24 * this.Hourly; 30 | static Weekly = 7 * this.Daily; 31 | static Monthly = 30 * this.Daily; 32 | static Yearly = 12 * this.Monthly; 33 | 34 | static assert(time_option) { 35 | if (![this.Minutely, this.Hourly, this.Daily, this.Weekly, this.Monthly, this.Yearly].includes(time_option)) { 36 | throw new Error( 37 | `time_option must be an instance of RollingConfig. Unsupported param ${JSON.stringify(time_option)}` 38 | ); 39 | } 40 | } 41 | } 42 | 43 | module.exports = { 44 | RollingSizeOptions, 45 | RollingTimeOptions, 46 | }; 47 | -------------------------------------------------------------------------------- /src/chapter_04.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logtar", 3 | "version": "0.0.6", 4 | "description": "A lightweight logger for your applications with rolling file support and much more.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "logger", 11 | "fast", 12 | "stream", 13 | "rolling", 14 | "json" 15 | ], 16 | "author": "Ishtmeet Singh ", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/ishtms/logtar.git" 20 | }, 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ishtms/logtar/issues" 24 | }, 25 | "homepage": "https://github.com/ishtms/logtar" 26 | } 27 | -------------------------------------------------------------------------------- /src/chapter_04.2/README.md: -------------------------------------------------------------------------------- 1 | ## Code for the Chapter 04.2 - Writing logs 2 | -------------------------------------------------------------------------------- /src/chapter_04.2/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "level": 3, 3 | "file_prefix": "LogTar_", 4 | "rolling_config": { 5 | "size_threshold": 1024000, 6 | "time_threshold": 86400 7 | } 8 | } -------------------------------------------------------------------------------- /src/chapter_04.2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/logtar') -------------------------------------------------------------------------------- /src/chapter_04.2/lib/config/log-config.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs"); 2 | 3 | const { LogLevel } = require("../utils/log-level"); 4 | const { RollingConfig } = require("./rolling-config"); 5 | 6 | class LogConfig { 7 | /** 8 | * @type {LogLevel} 9 | * @private 10 | * @description The log level to be used. 11 | */ 12 | #level = LogLevel.Info; 13 | 14 | /** 15 | * @type {RollingConfig} 16 | * @private 17 | */ 18 | #rolling_config; 19 | 20 | /** 21 | * @type {string} 22 | * @private 23 | * @description The prefix to be used for the log file name. 24 | * 25 | * If the file prefix is `MyFilePrefix_` the log files created will have the name 26 | * `MyFilePrefix_2021-09-01.log`, `MyFilePrefix_2021-09-02.log` and so on. 27 | */ 28 | #file_prefix = "Logtar_"; 29 | 30 | constructor() { 31 | this.#rolling_config = RollingConfig.with_defaults(); 32 | } 33 | 34 | /** 35 | * @returns {LogConfig} A new instance of LogConfig with default values. 36 | */ 37 | static with_defaults() { 38 | return new LogConfig(); 39 | } 40 | 41 | /** 42 | * @param {string} file_path The path to the config file. 43 | * @returns {LogConfig} A new instance of LogConfig with values from the config file. 44 | * @throws {Error} If the file_path is not a string. 45 | */ 46 | static from_file(file_path) { 47 | const file_contents = fs.readFileSync(file_path); 48 | return LogConfig.from_json(JSON.parse(file_contents)); 49 | } 50 | 51 | /** 52 | * @param {Object} json The json object to be parsed into {LogConfig}. 53 | * @returns {LogConfig} A new instance of LogConfig with values from the json object. 54 | */ 55 | static from_json(json) { 56 | let log_config = new LogConfig(); 57 | Object.keys(json).forEach((key) => { 58 | switch (key) { 59 | case "level": 60 | log_config = log_config.with_log_level(json[key]); 61 | break; 62 | case "rolling_config": 63 | log_config = log_config.with_rolling_config(json[key]); 64 | break; 65 | case "file_prefix": 66 | log_config = log_config.with_file_prefix(json[key]); 67 | break; 68 | } 69 | }); 70 | return log_config; 71 | } 72 | 73 | /** 74 | * @param {LogConfig} log_config The log config to be validated. 75 | * @throws {Error} If the log_config is not an instance of LogConfig. 76 | */ 77 | static assert(log_config) { 78 | if (arguments.length > 0 && !(log_config instanceof LogConfig)) { 79 | throw new Error( 80 | `log_config must be an instance of LogConfig. Unsupported param ${JSON.stringify(log_config)}` 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * @returns {LogLevel} The current log level. 87 | */ 88 | get level() { 89 | return this.#level; 90 | } 91 | 92 | /** 93 | * @param {LogLevel} log_level The log level to be set. 94 | * @returns {LogConfig} The current instance of LogConfig. 95 | * @throws {Error} If the log_level is not an instance of LogLevel. 96 | */ 97 | with_log_level(log_level) { 98 | LogLevel.assert(log_level); 99 | this.#level = log_level; 100 | return this; 101 | } 102 | 103 | /** 104 | * @returns {RollingConfig} The current rolling config. 105 | */ 106 | get rolling_config() { 107 | return this.#rolling_config; 108 | } 109 | 110 | /** 111 | * @param {RollingConfig} config The rolling config to be set. 112 | * @returns {LogConfig} The current instance of LogConfig. 113 | * @throws {Error} If the config is not an instance of RollingConfig. 114 | */ 115 | with_rolling_config(config) { 116 | this.#rolling_config = RollingConfig.from_json(config); 117 | return this; 118 | } 119 | 120 | /** 121 | * @returns {String} The current max file size. 122 | */ 123 | get file_prefix() { 124 | return this.#file_prefix; 125 | } 126 | 127 | /** 128 | * @param {string} file_prefix The file prefix to be set. 129 | * @returns {LogConfig} The current instance of LogConfig. 130 | * @throws {Error} If the file_prefix is not a string. 131 | */ 132 | with_file_prefix(file_prefix) { 133 | if (typeof file_prefix !== "string") { 134 | throw new Error(`file_prefix must be a string. Unsupported param ${JSON.stringify(file_prefix)}`); 135 | } 136 | 137 | this.#file_prefix = file_prefix; 138 | return this; 139 | } 140 | } 141 | 142 | module.exports = { LogConfig }; 143 | -------------------------------------------------------------------------------- /src/chapter_04.2/lib/config/rolling-config.js: -------------------------------------------------------------------------------- 1 | const { RollingTimeOptions, RollingSizeOptions } = require("../utils/rolling-options"); 2 | 3 | class RollingConfig { 4 | /** 5 | * Roll/Create new file every time the current file size exceeds this threshold in `seconds`. 6 | * 7 | * @type {RollingTimeOptions} 8 | * @private 9 | * 10 | */ 11 | #time_threshold = RollingTimeOptions.Hourly; 12 | 13 | /** 14 | * @type {RollingSizeOptions} 15 | * @private 16 | */ 17 | #size_threshold = RollingSizeOptions.FiveMB; 18 | 19 | /** 20 | * @returns {RollingConfig} A new instance of RollingConfig with default values. 21 | */ 22 | static with_defaults() { 23 | return new RollingConfig(); 24 | } 25 | 26 | /** 27 | * @param {number} size_threshold Roll/Create new file every time the current file size exceeds this threshold. 28 | * @returns {RollingConfig} The current instance of RollingConfig. 29 | */ 30 | with_size_threshold(size_threshold) { 31 | RollingSizeOptions.assert(size_threshold); 32 | this.#size_threshold = size_threshold; 33 | return this; 34 | } 35 | 36 | /** 37 | * @param {time_threshold} time_threshold Roll/Create new file every time the current file size exceeds this threshold. 38 | * @returns {RollingConfig} The current instance of RollingConfig. 39 | * @throws {Error} If the time_threshold is not an instance of RollingTimeOptions. 40 | */ 41 | with_time_threshold(time_threshold) { 42 | RollingTimeOptions.assert(time_threshold); 43 | this.#time_threshold = time_threshold; 44 | return this; 45 | } 46 | 47 | /** 48 | * @param {Object} json The json object to be parsed into {RollingConfig}. 49 | * @returns {RollingConfig} A new instance of RollingConfig with values from the json object. 50 | * @throws {Error} If the json is not an object. 51 | */ 52 | static from_json(json) { 53 | let rolling_config = new RollingConfig(); 54 | 55 | Object.keys(json).forEach((key) => { 56 | switch (key) { 57 | case "size_threshold": 58 | rolling_config = rolling_config.with_size_threshold(json[key]); 59 | break; 60 | case "time_threshold": 61 | rolling_config = rolling_config.with_time_threshold(json[key]); 62 | break; 63 | } 64 | }); 65 | 66 | return rolling_config; 67 | } 68 | 69 | /** 70 | * @returns {RollingTimeOptions} The current time threshold. 71 | */ 72 | get time_threshold() { 73 | return this.#time_threshold; 74 | } 75 | 76 | /** 77 | * @returns {RollingSizeOptions} The current size threshold. 78 | */ 79 | get size_threshold() { 80 | return this.#size_threshold; 81 | } 82 | } 83 | 84 | module.exports = { RollingConfig }; 85 | -------------------------------------------------------------------------------- /src/chapter_04.2/lib/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs/promises"); 2 | const path = require("node:path"); 3 | 4 | const { LogConfig } = require("./config/log-config"); 5 | const { LogLevel } = require("./utils/log-level"); 6 | const { check_and_create_dir } = require("./utils/helpers"); 7 | 8 | class Logger { 9 | /** 10 | * @type {LogConfig} 11 | */ 12 | #config; 13 | 14 | /** 15 | * @type {fs.FileHandle} 16 | */ 17 | #log_file_handle; 18 | 19 | /** 20 | * @param {LogLevel} log_level 21 | */ 22 | constructor(log_config) { 23 | log_config = log_config || LogConfig.with_defaults(); 24 | LogConfig.assert(log_config); 25 | this.#config = log_config; 26 | } 27 | 28 | async init() { 29 | const log_dir_path = check_and_create_dir("logs"); 30 | 31 | const file_name = this.#config.file_prefix + new Date().toISOString().replace(/[\.:]+/g, "-") + ".log"; 32 | this.#log_file_handle = await fs.open(path.join(log_dir_path, file_name), "a+"); 33 | } 34 | 35 | /** 36 | * @returns {Logger} A new instance of Logger with default config. 37 | */ 38 | static with_defaults() { 39 | return new Logger(); 40 | } 41 | 42 | /** 43 | * 44 | * @param {LogConfig} log_config 45 | * @returns {Logger} A new instance of Logger with the given config. 46 | */ 47 | static with_config(log_config) { 48 | return new Logger(log_config); 49 | } 50 | 51 | /** 52 | * @param {string} message 53 | * @param {number} log_level 54 | */ 55 | async #log(message, log_level) { 56 | if (log_level < this.#config.level || !this.#log_file_handle.fd) { 57 | return; 58 | } 59 | 60 | await this.#log_file_handle.write(message); 61 | } 62 | 63 | /** 64 | * @param {string} message 65 | */ 66 | debug(message) { 67 | this.#log(message, LogLevel.Debug); 68 | } 69 | 70 | /** 71 | * @param {string} message 72 | */ 73 | info(message) { 74 | this.#log(message, LogLevel.Info); 75 | } 76 | 77 | /** 78 | * @param {string} message 79 | */ 80 | warn(message) { 81 | this.#log(message, LogLevel.Warn); 82 | } 83 | 84 | /** 85 | * @param {string} message 86 | */ 87 | error(message) { 88 | this.#log(message, LogLevel.Error); 89 | } 90 | 91 | /** 92 | * @param {string} message 93 | */ 94 | critical(message) { 95 | this.#log(message, LogLevel.Critical); 96 | } 97 | 98 | /** Getters */ 99 | 100 | /** 101 | * @returns {LogLevel} The current log level. 102 | */ 103 | get level() { 104 | return this.#config.level; 105 | } 106 | 107 | /** 108 | * @returns {string} The log file prefix 109 | */ 110 | get file_prefix() { 111 | return this.#config.file_prefix; 112 | } 113 | 114 | /** 115 | * @returns {RollingTimeOptions} 116 | */ 117 | get time_threshold() { 118 | return this.#config.rolling_config.time_threshold; 119 | } 120 | 121 | /** 122 | * @returns {RollingSizeOptions} 123 | */ 124 | get size_threshold() { 125 | return this.#config.rolling_config.size_threshold; 126 | } 127 | } 128 | 129 | module.exports = { Logger }; 130 | -------------------------------------------------------------------------------- /src/chapter_04.2/lib/logtar.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Logger: require('./logger').Logger, 3 | LogConfig: require('./config/log-config').LogConfig, 4 | RollingConfig: require('./config/rolling-config').RollingConfig, 5 | LogLevel: require('./utils/log-level').LogLevel, 6 | RollingTimeOptions: require('./utils/rolling-options').RollingTimeOptions, 7 | RollingSizeOptions: require('./utils/rolling-options').RollingSizeOptions, 8 | }; 9 | -------------------------------------------------------------------------------- /src/chapter_04.2/lib/utils/helpers.js: -------------------------------------------------------------------------------- 1 | const fs_sync = require('node:fs'); 2 | const path = require('path') 3 | 4 | /** 5 | * @returns {fs_sync.PathLike} The path to the directory. 6 | */ 7 | function check_and_create_dir(path_to_dir) { 8 | const log_dir = path.resolve(require.main.path, path_to_dir); 9 | if (!fs_sync.existsSync(log_dir)) { 10 | fs_sync.mkdirSync(log_dir, { recursive: true }); 11 | } 12 | 13 | return log_dir 14 | } 15 | 16 | module.exports = { 17 | check_and_create_dir 18 | } -------------------------------------------------------------------------------- /src/chapter_04.2/lib/utils/log-level.js: -------------------------------------------------------------------------------- 1 | class LogLevel { 2 | static #Debug = 0; 3 | static #Info = 1; 4 | static #Warn = 2; 5 | static #Error = 3; 6 | static #Critical = 4; 7 | 8 | /** 9 | * @param {number} log_level 10 | */ 11 | static to_string(log_level) { 12 | const levelMap = { 13 | [this.Debug]: "DEBUG", 14 | [this.Info]: "INFO", 15 | [this.Warn]: "WARN", 16 | [this.Error]: "ERROR", 17 | [this.Critical]: "CRITICAL" 18 | }; 19 | 20 | if (levelMap.hasOwnProperty(log_level)) { 21 | return levelMap[log_level]; 22 | } 23 | 24 | throw new Error(`Unsupported log level ${log_level}`); 25 | } 26 | 27 | static get Debug() { 28 | return this.#Debug; 29 | } 30 | 31 | static get Info() { 32 | return this.#Info; 33 | } 34 | 35 | static get Warn() { 36 | return this.#Warn; 37 | } 38 | 39 | static get Error() { 40 | return this.#Error; 41 | } 42 | 43 | static get Critical() { 44 | return this.#Critical; 45 | } 46 | 47 | static assert(log_level) { 48 | if (![this.Debug, this.Info, this.Warn, this.Error, this.Critical].includes(log_level)) { 49 | throw new Error( 50 | `log_level must be an instance of LogLevel. Unsupported param ${JSON.stringify(log_level)}` 51 | ); 52 | } 53 | } 54 | } 55 | 56 | module.exports = { LogLevel }; 57 | -------------------------------------------------------------------------------- /src/chapter_04.2/lib/utils/rolling-options.js: -------------------------------------------------------------------------------- 1 | class RollingSizeOptions { 2 | static OneKB = 1024; 3 | static FiveKB = 5 * 1024; 4 | static TenKB = 10 * 1024; 5 | static TwentyKB = 20 * 1024; 6 | static FiftyKB = 50 * 1024; 7 | static HundredKB = 100 * 1024; 8 | 9 | static HalfMB = 512 * 1024; 10 | static OneMB = 1024 * 1024; 11 | static FiveMB = 5 * 1024 * 1024; 12 | static TenMB = 10 * 1024 * 1024; 13 | static TwentyMB = 20 * 1024 * 1024; 14 | static FiftyMB = 50 * 1024 * 1024; 15 | static HundredMB = 100 * 1024 * 1024; 16 | 17 | static assert(size_threshold) { 18 | if (typeof size_threshold !== "number" || size_threshold < RollingSizeOptions.OneKB) { 19 | throw new Error( 20 | `size_threshold must be at-least 1 KB. Unsupported param ${JSON.stringify(size_threshold)}` 21 | ); 22 | } 23 | } 24 | } 25 | 26 | class RollingTimeOptions { 27 | static Minutely = 60; // Every 60 seconds 28 | static Hourly = 60 * this.Minutely; 29 | static Daily = 24 * this.Hourly; 30 | static Weekly = 7 * this.Daily; 31 | static Monthly = 30 * this.Daily; 32 | static Yearly = 12 * this.Monthly; 33 | 34 | static assert(time_option) { 35 | if (![this.Minutely, this.Hourly, this.Daily, this.Weekly, this.Monthly, this.Yearly].includes(time_option)) { 36 | throw new Error( 37 | `time_option must be an instance of RollingConfig. Unsupported param ${JSON.stringify(time_option)}` 38 | ); 39 | } 40 | } 41 | } 42 | 43 | module.exports = { 44 | RollingSizeOptions, 45 | RollingTimeOptions, 46 | }; 47 | -------------------------------------------------------------------------------- /src/chapter_04.2/logs/LogTar_2023-08-18T19-48-03.log: -------------------------------------------------------------------------------- 1 | This is an error message -------------------------------------------------------------------------------- /src/chapter_04.2/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logtar", 3 | "version": "0.0.2", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "logtar", 9 | "version": "0.0.2", 10 | "license": "MIT" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/chapter_04.2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logtar", 3 | "version": "0.0.7", 4 | "description": "A lightweight logger for your applications with rolling file support and much more.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node test.js" 8 | }, 9 | "keywords": [ 10 | "logger", 11 | "fast", 12 | "stream", 13 | "rolling", 14 | "json" 15 | ], 16 | "author": "Ishtmeet Singh ", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/ishtms/logtar.git" 20 | }, 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ishtms/logtar/issues" 24 | }, 25 | "homepage": "https://github.com/ishtms/logtar" 26 | } 27 | -------------------------------------------------------------------------------- /src/chapter_04.3/README.md: -------------------------------------------------------------------------------- 1 | ## Code for the Chapter 04.3 - Capturing Metadata 2 | -------------------------------------------------------------------------------- /src/chapter_04.3/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/logtar"); 2 | -------------------------------------------------------------------------------- /src/chapter_04.3/lib/config/log-config.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs"); 2 | 3 | const { LogLevel } = require("../utils/log-level"); 4 | const { RollingConfig } = require("./rolling-config"); 5 | 6 | class LogConfig { 7 | /** 8 | * @type {LogLevel} 9 | * @private 10 | * @description The log level to be used. 11 | */ 12 | #level = LogLevel.Info; 13 | 14 | /** 15 | * @type {RollingConfig} 16 | * @private 17 | */ 18 | #rolling_config; 19 | 20 | /** 21 | * @type {string} 22 | * @private 23 | * @description The prefix to be used for the log file name. 24 | * 25 | * If the file prefix is `MyFilePrefix_` the log files created will have the name 26 | * `MyFilePrefix_2021-09-01.log`, `MyFilePrefix_2021-09-02.log` and so on. 27 | */ 28 | #file_prefix = "Logtar_"; 29 | 30 | constructor() { 31 | this.#rolling_config = RollingConfig.with_defaults(); 32 | } 33 | 34 | /** 35 | * @returns {LogConfig} A new instance of LogConfig with default values. 36 | */ 37 | static with_defaults() { 38 | return new LogConfig(); 39 | } 40 | 41 | /** 42 | * @param {string} file_path The path to the config file. 43 | * @returns {LogConfig} A new instance of LogConfig with values from the config file. 44 | * @throws {Error} If the file_path is not a string. 45 | */ 46 | static from_file(file_path) { 47 | const file_contents = fs.readFileSync(file_path); 48 | return LogConfig.from_json(JSON.parse(file_contents)); 49 | } 50 | 51 | /** 52 | * @param {Object} json The json object to be parsed into {LogConfig}. 53 | * @returns {LogConfig} A new instance of LogConfig with values from the json object. 54 | */ 55 | static from_json(json) { 56 | let log_config = new LogConfig(); 57 | Object.keys(json).forEach((key) => { 58 | switch (key) { 59 | case "level": 60 | log_config = log_config.with_log_level(json[key]); 61 | break; 62 | case "rolling_config": 63 | log_config = log_config.with_rolling_config(json[key]); 64 | break; 65 | case "file_prefix": 66 | log_config = log_config.with_file_prefix(json[key]); 67 | break; 68 | } 69 | }); 70 | return log_config; 71 | } 72 | 73 | /** 74 | * @param {LogConfig} log_config The log config to be validated. 75 | * @throws {Error} If the log_config is not an instance of LogConfig. 76 | */ 77 | static assert(log_config) { 78 | if (arguments.length > 0 && !(log_config instanceof LogConfig)) { 79 | throw new Error( 80 | `log_config must be an instance of LogConfig. Unsupported param ${JSON.stringify(log_config)}` 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * @returns {LogLevel} The current log level. 87 | */ 88 | get level() { 89 | return this.#level; 90 | } 91 | 92 | /** 93 | * @param {LogLevel} log_level The log level to be set. 94 | * @returns {LogConfig} The current instance of LogConfig. 95 | * @throws {Error} If the log_level is not an instance of LogLevel. 96 | */ 97 | with_log_level(log_level) { 98 | LogLevel.assert(log_level); 99 | this.#level = log_level; 100 | return this; 101 | } 102 | 103 | /** 104 | * @returns {RollingConfig} The current rolling config. 105 | */ 106 | get rolling_config() { 107 | return this.#rolling_config; 108 | } 109 | 110 | /** 111 | * @param {RollingConfig} config The rolling config to be set. 112 | * @returns {LogConfig} The current instance of LogConfig. 113 | * @throws {Error} If the config is not an instance of RollingConfig. 114 | */ 115 | with_rolling_config(config) { 116 | this.#rolling_config = RollingConfig.from_json(config); 117 | return this; 118 | } 119 | 120 | /** 121 | * @returns {String} The current max file size. 122 | */ 123 | get file_prefix() { 124 | return this.#file_prefix; 125 | } 126 | 127 | /** 128 | * @param {string} file_prefix The file prefix to be set. 129 | * @returns {LogConfig} The current instance of LogConfig. 130 | * @throws {Error} If the file_prefix is not a string. 131 | */ 132 | with_file_prefix(file_prefix) { 133 | if (typeof file_prefix !== "string") { 134 | throw new Error(`file_prefix must be a string. Unsupported param ${JSON.stringify(file_prefix)}`); 135 | } 136 | 137 | this.#file_prefix = file_prefix; 138 | return this; 139 | } 140 | } 141 | 142 | module.exports = { LogConfig }; 143 | -------------------------------------------------------------------------------- /src/chapter_04.3/lib/config/rolling-config.js: -------------------------------------------------------------------------------- 1 | const { RollingTimeOptions, RollingSizeOptions } = require("../utils/rolling-options"); 2 | 3 | class RollingConfig { 4 | /** 5 | * Roll/Create new file every time the current file size exceeds this threshold in `seconds`. 6 | * 7 | * @type {RollingTimeOptions} 8 | * @private 9 | * 10 | */ 11 | #time_threshold = RollingTimeOptions.Hourly; 12 | 13 | /** 14 | * @type {RollingSizeOptions} 15 | * @private 16 | */ 17 | #size_threshold = RollingSizeOptions.FiveMB; 18 | 19 | /** 20 | * @returns {RollingConfig} A new instance of RollingConfig with default values. 21 | */ 22 | static with_defaults() { 23 | return new RollingConfig(); 24 | } 25 | 26 | /** 27 | * @param {number} size_threshold Roll/Create new file every time the current file size exceeds this threshold. 28 | * @returns {RollingConfig} The current instance of RollingConfig. 29 | */ 30 | with_size_threshold(size_threshold) { 31 | RollingSizeOptions.assert(size_threshold); 32 | this.#size_threshold = size_threshold; 33 | return this; 34 | } 35 | 36 | /** 37 | * @param {time_threshold} time_threshold Roll/Create new file every time the current file size exceeds this threshold. 38 | * @returns {RollingConfig} The current instance of RollingConfig. 39 | * @throws {Error} If the time_threshold is not an instance of RollingTimeOptions. 40 | */ 41 | with_time_threshold(time_threshold) { 42 | RollingTimeOptions.assert(time_threshold); 43 | this.#time_threshold = time_threshold; 44 | return this; 45 | } 46 | 47 | /** 48 | * @param {Object} json The json object to be parsed into {RollingConfig}. 49 | * @returns {RollingConfig} A new instance of RollingConfig with values from the json object. 50 | * @throws {Error} If the json is not an object. 51 | */ 52 | static from_json(json) { 53 | let rolling_config = new RollingConfig(); 54 | 55 | Object.keys(json).forEach((key) => { 56 | switch (key) { 57 | case "size_threshold": 58 | rolling_config = rolling_config.with_size_threshold(json[key]); 59 | break; 60 | case "time_threshold": 61 | rolling_config = rolling_config.with_time_threshold(json[key]); 62 | break; 63 | } 64 | }); 65 | 66 | return rolling_config; 67 | } 68 | 69 | /** 70 | * @returns {RollingTimeOptions} The current time threshold. 71 | */ 72 | get time_threshold() { 73 | return this.#time_threshold; 74 | } 75 | 76 | /** 77 | * @returns {RollingSizeOptions} The current size threshold. 78 | */ 79 | get size_threshold() { 80 | return this.#size_threshold; 81 | } 82 | } 83 | 84 | module.exports = { RollingConfig }; 85 | -------------------------------------------------------------------------------- /src/chapter_04.3/lib/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs/promises"); 2 | const path = require("node:path"); 3 | 4 | const { LogConfig } = require("./config/log-config"); 5 | const { LogLevel } = require("./utils/log-level"); 6 | const { RollingTimeOptions } = require("./utils/rolling-options"); 7 | const { check_and_create_dir, get_caller_info } = require("./utils/helpers"); 8 | 9 | class Logger { 10 | /** 11 | * @type {LogConfig} 12 | */ 13 | #config; 14 | 15 | /** 16 | * @type {fs.FileHandle} 17 | */ 18 | #log_file_handle; 19 | 20 | /** 21 | * @param {LogLevel} log_level 22 | */ 23 | constructor(log_config) { 24 | log_config = log_config || LogConfig.with_defaults(); 25 | LogConfig.assert(log_config); 26 | this.#config = log_config; 27 | this.#init(); 28 | } 29 | 30 | /** 31 | * Initializes the logger by setting up the process exit handlers. 32 | */ 33 | #init() { 34 | process.on("exit", this.#setup_for_process_exit.bind(this)); 35 | process.on("SIGINT", this.#setup_for_process_exit.bind(this)); 36 | process.on("SIGTERM", this.#setup_for_process_exit.bind(this)); 37 | // you can't catch SIGKILL 38 | //process.on("SIGKILL", this.#setup_for_process_exit.bind(this)); 39 | } 40 | 41 | /** 42 | * Initializes the logger by creating the log file, and directory if they don't exist. 43 | */ 44 | async init() { 45 | const log_dir_path = check_and_create_dir("logs"); 46 | 47 | const file_name = this.#config.file_prefix + new Date().toISOString().replace(/[\.:]+/g, "-") + ".log"; 48 | this.#log_file_handle = await fs.open(path.join(log_dir_path, file_name), "a+"); 49 | } 50 | 51 | /** 52 | * @param {number} signal The exit signal received by the process. 53 | */ 54 | async #setup_for_process_exit(signal) { 55 | if (this.#log_file_handle.fd <= 0) return; 56 | 57 | this.critical(`Logger shutting down. Received signal: ${signal}`); 58 | await this.#log_file_handle.sync(); 59 | await this.#log_file_handle.close(); 60 | process.exit(); 61 | } 62 | 63 | /** 64 | * @returns {Logger} A new instance of Logger with default config. 65 | */ 66 | static with_defaults() { 67 | return new Logger(); 68 | } 69 | 70 | /** 71 | * 72 | * @param {LogConfig} log_config 73 | * @returns {Logger} A new instance of Logger with the given config. 74 | */ 75 | static with_config(log_config) { 76 | return new Logger(log_config); 77 | } 78 | 79 | /** 80 | * Writes the given message to the log file. 81 | * @private 82 | * @param {string} message 83 | * @param {number} log_level 84 | */ 85 | async #log(message, log_level) { 86 | if (log_level < this.#config.level || !this.#log_file_handle.fd) { 87 | return; 88 | } 89 | 90 | const date_iso = new Date().toISOString(); 91 | const log_level_string = LogLevel.to_string(log_level); 92 | 93 | const log_message = `[${date_iso}] [${log_level_string}]: ${get_caller_info()} ${message}\n`; 94 | await this.#log_file_handle.write(log_message); 95 | } 96 | 97 | /** 98 | * @param {string} message 99 | */ 100 | debug(message) { 101 | this.#log(message, LogLevel.Debug); 102 | } 103 | 104 | /** 105 | * @param {string} message 106 | */ 107 | info(message) { 108 | this.#log(message, LogLevel.Info); 109 | } 110 | 111 | /** 112 | * @param {string} message 113 | */ 114 | warn(message) { 115 | this.#log(message, LogLevel.Warn); 116 | } 117 | 118 | /** 119 | * @param {string} message 120 | */ 121 | error(message) { 122 | this.#log(message, LogLevel.Error); 123 | } 124 | 125 | /** 126 | * @param {string} message 127 | */ 128 | critical(message) { 129 | this.#log(message, LogLevel.Critical); 130 | } 131 | 132 | /** Getters */ 133 | 134 | /** 135 | * @returns {LogLevel} The current log level. 136 | */ 137 | get level() { 138 | return this.#config.level; 139 | } 140 | 141 | /** 142 | * @returns {string} The log file prefix 143 | */ 144 | get file_prefix() { 145 | return this.#config.file_prefix; 146 | } 147 | 148 | /** 149 | * @returns {RollingTimeOptions} 150 | */ 151 | get time_threshold() { 152 | return this.#config.rolling_config.time_threshold; 153 | } 154 | 155 | /** 156 | * @returns {RollingSizeOptions} 157 | */ 158 | get size_threshold() { 159 | return this.#config.rolling_config.size_threshold; 160 | } 161 | } 162 | 163 | module.exports = { Logger }; 164 | -------------------------------------------------------------------------------- /src/chapter_04.3/lib/logtar.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Logger: require('./logger').Logger, 3 | LogConfig: require('./config/log-config').LogConfig, 4 | RollingConfig: require('./config/rolling-config').RollingConfig, 5 | LogLevel: require('./utils/log-level').LogLevel, 6 | RollingTimeOptions: require('./utils/rolling-options').RollingTimeOptions, 7 | RollingSizeOptions: require('./utils/rolling-options').RollingSizeOptions, 8 | }; -------------------------------------------------------------------------------- /src/chapter_04.3/lib/utils/helpers.js: -------------------------------------------------------------------------------- 1 | const fs_sync = require('node:fs'); 2 | const path = require('path') 3 | 4 | /** 5 | * @returns {fs_sync.PathLike} The path to the directory. 6 | */ 7 | function check_and_create_dir(path_to_dir) { 8 | const log_dir = path.resolve(require.main.path, path_to_dir); 9 | if (!fs_sync.existsSync(log_dir)) { 10 | fs_sync.mkdirSync(log_dir, { recursive: true }); 11 | } 12 | 13 | return log_dir 14 | } 15 | 16 | /** 17 | * @returns {string} The meta data of the caller by parsing the stack trace. 18 | */ 19 | function get_caller_info() { 20 | const error = {}; 21 | Error.captureStackTrace(error); 22 | 23 | const caller_frame = error.stack.split("\n")[4]; 24 | 25 | const meta_data = caller_frame.split("at ").pop(); 26 | return meta_data 27 | } 28 | 29 | module.exports = { 30 | check_and_create_dir, 31 | get_caller_info 32 | } -------------------------------------------------------------------------------- /src/chapter_04.3/lib/utils/log-level.js: -------------------------------------------------------------------------------- 1 | class LogLevel { 2 | static #Debug = 0; 3 | static #Info = 1; 4 | static #Warn = 2; 5 | static #Error = 3; 6 | static #Critical = 4; 7 | 8 | /** 9 | * @param {number} log_level 10 | */ 11 | static to_string(log_level) { 12 | const levelMap = { 13 | [this.Debug]: "DEBUG", 14 | [this.Info]: "INFO", 15 | [this.Warn]: "WARN", 16 | [this.Error]: "ERROR", 17 | [this.Critical]: "CRITICAL" 18 | }; 19 | 20 | if (levelMap.hasOwnProperty(log_level)) { 21 | return levelMap[log_level]; 22 | } 23 | 24 | throw new Error(`Unsupported log level ${log_level}`); 25 | } 26 | 27 | static get Debug() { 28 | return this.#Debug; 29 | } 30 | 31 | static get Info() { 32 | return this.#Info; 33 | } 34 | 35 | static get Warn() { 36 | return this.#Warn; 37 | } 38 | 39 | static get Error() { 40 | return this.#Error; 41 | } 42 | 43 | static get Critical() { 44 | return this.#Critical; 45 | } 46 | 47 | static assert(log_level) { 48 | if (![this.Debug, this.Info, this.Warn, this.Error, this.Critical].includes(log_level)) { 49 | throw new Error( 50 | `log_level must be an instance of LogLevel. Unsupported param ${JSON.stringify(log_level)}` 51 | ); 52 | } 53 | } 54 | } 55 | 56 | module.exports = { LogLevel }; 57 | -------------------------------------------------------------------------------- /src/chapter_04.3/lib/utils/rolling-options.js: -------------------------------------------------------------------------------- 1 | class RollingSizeOptions { 2 | static OneKB = 1024; 3 | static FiveKB = 5 * 1024; 4 | static TenKB = 10 * 1024; 5 | static TwentyKB = 20 * 1024; 6 | static FiftyKB = 50 * 1024; 7 | static HundredKB = 100 * 1024; 8 | 9 | static HalfMB = 512 * 1024; 10 | static OneMB = 1024 * 1024; 11 | static FiveMB = 5 * 1024 * 1024; 12 | static TenMB = 10 * 1024 * 1024; 13 | static TwentyMB = 20 * 1024 * 1024; 14 | static FiftyMB = 50 * 1024 * 1024; 15 | static HundredMB = 100 * 1024 * 1024; 16 | 17 | static assert(size_threshold) { 18 | if (typeof size_threshold !== "number" || size_threshold < RollingSizeOptions.OneKB) { 19 | throw new Error( 20 | `size_threshold must be at-least 1 KB. Unsupported param ${JSON.stringify(size_threshold)}` 21 | ); 22 | } 23 | } 24 | } 25 | 26 | class RollingTimeOptions { 27 | static Minutely = 60; // Every 60 seconds 28 | static Hourly = 60 * this.Minutely; 29 | static Daily = 24 * this.Hourly; 30 | static Weekly = 7 * this.Daily; 31 | static Monthly = 30 * this.Daily; 32 | static Yearly = 12 * this.Monthly; 33 | 34 | static assert(time_option) { 35 | if (![this.Minutely, this.Hourly, this.Daily, this.Weekly, this.Monthly, this.Yearly].includes(time_option)) { 36 | throw new Error( 37 | `time_option must be an instance of RollingConfig. Unsupported param ${JSON.stringify(time_option)}` 38 | ); 39 | } 40 | } 41 | } 42 | 43 | module.exports = { 44 | RollingSizeOptions, 45 | RollingTimeOptions, 46 | }; 47 | -------------------------------------------------------------------------------- /src/chapter_04.3/logs/Logtar_2023-08-19T19-11-51.log: -------------------------------------------------------------------------------- 1 | [2023-08-19T19:11:51.888Z] [CRITICAL]: main (/Users/ishtmeet/Code/logtard/test.js:11:12) From the main() function 2 | [2023-08-19T19:11:51.888Z] [CRITICAL]: nested_func (/Users/ishtmeet/Code/logtard/test.js:16:12) From the nested_func() function 3 | [2023-08-19T19:11:51.888Z] [CRITICAL]: super_nested (/Users/ishtmeet/Code/logtard/test.js:21:12) From the super_nested() function 4 | [2023-08-19T19:11:51.888Z] [CRITICAL]: Logger.#setup_for_process_exit (/Users/ishtmeet/Code/logtard/lib/logger.js:50:14) Logger shutting down. Received signal: 0 5 | -------------------------------------------------------------------------------- /src/chapter_04.3/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logtar", 3 | "version": "0.0.2", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "logtar", 9 | "version": "0.0.2", 10 | "license": "MIT" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/chapter_04.3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logtar", 3 | "version": "0.0.8", 4 | "description": "A lightweight logger for your applications with rolling file support and much more.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "logger", 11 | "fast", 12 | "stream", 13 | "rolling", 14 | "json" 15 | ], 16 | "author": "Ishtmeet Singh ", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/ishtms/logtar.git" 20 | }, 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ishtms/logtar/issues" 24 | }, 25 | "homepage": "https://github.com/ishtms/logtar" 26 | } 27 | -------------------------------------------------------------------------------- /src/chapter_04.5/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/*/** 2 | test.js 3 | -------------------------------------------------------------------------------- /src/chapter_04.5/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } -------------------------------------------------------------------------------- /src/chapter_04.5/README.md: -------------------------------------------------------------------------------- 1 | ## Code for the Chapter 04.5 - Adding Rolling File Support 2 | -------------------------------------------------------------------------------- /src/chapter_04.5/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "level": 0, 3 | "log_prefix": "LogTar_", 4 | "rolling_config": { 5 | "size_threshold": 10240, 6 | "time_threshold": 60 7 | } 8 | } -------------------------------------------------------------------------------- /src/chapter_04.5/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/logtar') -------------------------------------------------------------------------------- /src/chapter_04.5/lib/config/log-config.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs"); 2 | 3 | const { LogLevel } = require("../utils/log-level"); 4 | const { RollingConfig } = require("./rolling-config"); 5 | 6 | class LogConfig { 7 | /** 8 | * @type {LogLevel} 9 | * @private 10 | * @description The log level to be used. 11 | */ 12 | #level = LogLevel.Info; 13 | 14 | /** 15 | * @type {RollingConfig} 16 | * @private 17 | */ 18 | #rolling_config; 19 | 20 | /** 21 | * @type {string} 22 | * @private 23 | * @description The prefix to be used for the log file name. 24 | * 25 | * If the file prefix is `MyFilePrefix_` the log files created will have the name 26 | * `MyFilePrefix_2021-09-01.log`, `MyFilePrefix_2021-09-02.log` and so on. 27 | */ 28 | #file_prefix = "Logtar_"; 29 | 30 | constructor() { 31 | this.#rolling_config = RollingConfig.with_defaults(); 32 | } 33 | 34 | /** 35 | * @returns {LogConfig} A new instance of LogConfig with default values. 36 | */ 37 | static with_defaults() { 38 | return new LogConfig(); 39 | } 40 | 41 | /** 42 | * @param {string} file_path The path to the config file. 43 | * @returns {LogConfig} A new instance of LogConfig with values from the config file. 44 | * @throws {Error} If the file_path is not a string. 45 | */ 46 | static from_file(file_path) { 47 | const file_contents = fs.readFileSync(file_path); 48 | return LogConfig.from_json(JSON.parse(file_contents)); 49 | } 50 | 51 | /** 52 | * @param {Object} json The json object to be parsed into {LogConfig}. 53 | * @returns {LogConfig} A new instance of LogConfig with values from the json object. 54 | */ 55 | static from_json(json) { 56 | let log_config = new LogConfig(); 57 | Object.keys(json).forEach((key) => { 58 | switch (key) { 59 | case "level": 60 | log_config = log_config.with_log_level(json[key]); 61 | break; 62 | case "rolling_config": 63 | log_config = log_config.with_rolling_config(json[key]); 64 | break; 65 | case "file_prefix": 66 | log_config = log_config.with_file_prefix(json[key]); 67 | break; 68 | } 69 | }); 70 | return log_config; 71 | } 72 | 73 | /** 74 | * @param {LogConfig} log_config The log config to be validated. 75 | * @throws {Error} If the log_config is not an instance of LogConfig. 76 | */ 77 | static assert(log_config) { 78 | if (arguments.length > 0 && !(log_config instanceof LogConfig)) { 79 | throw new Error( 80 | `log_config must be an instance of LogConfig. Unsupported param ${JSON.stringify(log_config)}` 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * @returns {LogLevel} The current log level. 87 | */ 88 | get level() { 89 | return this.#level; 90 | } 91 | 92 | /** 93 | * @param {LogLevel} log_level The log level to be set. 94 | * @returns {LogConfig} The current instance of LogConfig. 95 | * @throws {Error} If the log_level is not an instance of LogLevel. 96 | */ 97 | with_log_level(log_level) { 98 | LogLevel.assert(log_level); 99 | this.#level = log_level; 100 | return this; 101 | } 102 | 103 | /** 104 | * @returns {RollingConfig} The current rolling config. 105 | */ 106 | get rolling_config() { 107 | return this.#rolling_config; 108 | } 109 | 110 | /** 111 | * @param {RollingConfig} config The rolling config to be set. 112 | * @returns {LogConfig} The current instance of LogConfig. 113 | * @throws {Error} If the config is not an instance of RollingConfig. 114 | */ 115 | with_rolling_config(config) { 116 | this.#rolling_config = RollingConfig.from_json(config); 117 | return this; 118 | } 119 | 120 | /** 121 | * @returns {String} The current max file size. 122 | */ 123 | get file_prefix() { 124 | return this.#file_prefix; 125 | } 126 | 127 | /** 128 | * @param {string} file_prefix The file prefix to be set. 129 | * @returns {LogConfig} The current instance of LogConfig. 130 | * @throws {Error} If the file_prefix is not a string. 131 | */ 132 | with_file_prefix(file_prefix) { 133 | if (typeof file_prefix !== "string") { 134 | throw new Error(`file_prefix must be a string. Unsupported param ${JSON.stringify(file_prefix)}`); 135 | } 136 | 137 | this.#file_prefix = file_prefix; 138 | return this; 139 | } 140 | } 141 | 142 | module.exports = { LogConfig }; 143 | -------------------------------------------------------------------------------- /src/chapter_04.5/lib/config/rolling-config.js: -------------------------------------------------------------------------------- 1 | const { RollingTimeOptions, RollingSizeOptions } = require("../utils/rolling-options"); 2 | 3 | class RollingConfig { 4 | /** 5 | * Roll/Create new file every time the current file size exceeds this threshold in `seconds`. 6 | * 7 | * @type {RollingTimeOptions} 8 | * @private 9 | * 10 | */ 11 | #time_threshold = RollingTimeOptions.Hourly; 12 | 13 | /** 14 | * @type {RollingSizeOptions} 15 | * @private 16 | */ 17 | #size_threshold = RollingSizeOptions.FiveMB; 18 | 19 | /** 20 | * @returns {RollingConfig} A new instance of RollingConfig with default values. 21 | */ 22 | static with_defaults() { 23 | return new RollingConfig(); 24 | } 25 | 26 | /** 27 | * @param {number} size_threshold Roll/Create new file every time the current file size exceeds this threshold. 28 | * @returns {RollingConfig} The current instance of RollingConfig. 29 | */ 30 | with_size_threshold(size_threshold) { 31 | RollingSizeOptions.assert(size_threshold); 32 | this.#size_threshold = size_threshold; 33 | return this; 34 | } 35 | 36 | /** 37 | * @param {time_threshold} time_threshold Roll/Create new file every time the current file size exceeds this threshold. 38 | * @returns {RollingConfig} The current instance of RollingConfig. 39 | * @throws {Error} If the time_threshold is not an instance of RollingTimeOptions. 40 | */ 41 | with_time_threshold(time_threshold) { 42 | RollingTimeOptions.assert(time_threshold); 43 | this.#time_threshold = time_threshold; 44 | return this; 45 | } 46 | 47 | /** 48 | * @param {Object} json The json object to be parsed into {RollingConfig}. 49 | * @returns {RollingConfig} A new instance of RollingConfig with values from the json object. 50 | * @throws {Error} If the json is not an object. 51 | */ 52 | static from_json(json) { 53 | let rolling_config = new RollingConfig(); 54 | 55 | Object.keys(json).forEach((key) => { 56 | switch (key) { 57 | case "size_threshold": 58 | rolling_config = rolling_config.with_size_threshold(json[key]); 59 | break; 60 | case "time_threshold": 61 | rolling_config = rolling_config.with_time_threshold(json[key]); 62 | break; 63 | } 64 | }); 65 | 66 | return rolling_config; 67 | } 68 | 69 | /** 70 | * @returns {RollingTimeOptions} The current time threshold. 71 | */ 72 | get time_threshold() { 73 | return this.#time_threshold; 74 | } 75 | 76 | /** 77 | * @returns {RollingSizeOptions} The current size threshold. 78 | */ 79 | get size_threshold() { 80 | return this.#size_threshold; 81 | } 82 | } 83 | 84 | module.exports = { RollingConfig }; 85 | -------------------------------------------------------------------------------- /src/chapter_04.5/lib/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs/promises"); 2 | const path = require("node:path"); 3 | 4 | const { LogConfig } = require("./config/log-config"); 5 | const { LogLevel } = require("./utils/log-level"); 6 | const { RollingTimeOptions } = require("./utils/rolling-options"); 7 | const { check_and_create_dir, get_caller_info } = require("./utils/helpers"); 8 | 9 | class Logger { 10 | /** 11 | * @type {LogConfig} 12 | */ 13 | #config; 14 | 15 | /** 16 | * @type {fs.FileHandle} 17 | */ 18 | #log_file_handle; 19 | 20 | /** 21 | * @param {LogLevel} log_level 22 | */ 23 | constructor(log_config) { 24 | log_config = log_config || LogConfig.with_defaults(); 25 | LogConfig.assert(log_config); 26 | this.#config = log_config; 27 | } 28 | 29 | /** 30 | * Initializes the logger by creating the log file, and directory if they don't exist. 31 | */ 32 | async init() { 33 | const log_dir_path = check_and_create_dir("logs"); 34 | 35 | const file_name = this.#config.file_prefix + new Date().toISOString().replace(/[\.:]+/g, "-") + ".log"; 36 | this.#log_file_handle = await fs.open(path.join(log_dir_path, file_name), "a+"); 37 | } 38 | 39 | /** 40 | * @returns {Logger} A new instance of Logger with default config. 41 | */ 42 | static with_defaults() { 43 | return new Logger(); 44 | } 45 | 46 | /** 47 | * 48 | * @param {LogConfig} log_config 49 | * @returns {Logger} A new instance of Logger with the given config. 50 | */ 51 | static with_config(log_config) { 52 | return new Logger(log_config); 53 | } 54 | 55 | /** 56 | * Writes the given message to the log file. 57 | * @private 58 | * @param {string} message 59 | * @param {number} log_level 60 | */ 61 | async #log(message, log_level) { 62 | if (log_level < this.#config.level || !this.#log_file_handle.fd) { 63 | return; 64 | } 65 | 66 | await this.#write_to_handle(message, log_level); 67 | await this.#rolling_check(); 68 | } 69 | 70 | /** 71 | * Checks if the current log file needs to be rolled over. 72 | */ 73 | async #rolling_check() { 74 | const { size_threshold, time_threshold } = this.#config.rolling_config; 75 | 76 | const { size, birthtimeMs } = await this.#log_file_handle.stat(); 77 | const current_time = new Date().getTime(); 78 | 79 | if (size >= size_threshold || current_time - birthtimeMs >= time_threshold * 1000) { 80 | await this.#log_file_handle.close(); 81 | await this.init(); 82 | } 83 | } 84 | 85 | /** 86 | * Writes the given message to the log file. 87 | * @private 88 | * @param {string} message 89 | * @param {LogLevel} log_level 90 | * @returns {Promise} 91 | */ 92 | async #write_to_handle(message, log_level) { 93 | const date_iso = new Date().toISOString(); 94 | const log_level_string = LogLevel.to_string(log_level); 95 | 96 | const log_message = `[${date_iso}] [${log_level_string}]: ${get_caller_info()} ${message}\n`; 97 | await this.#log_file_handle.write(log_message); 98 | } 99 | 100 | /** 101 | * @param {string} message 102 | */ 103 | debug(message) { 104 | this.#log(message, LogLevel.Debug); 105 | } 106 | 107 | /** 108 | * @param {string} message 109 | */ 110 | info(message) { 111 | this.#log(message, LogLevel.Info); 112 | } 113 | 114 | /** 115 | * @param {string} message 116 | */ 117 | warn(message) { 118 | this.#log(message, LogLevel.Warn); 119 | } 120 | 121 | /** 122 | * @param {string} message 123 | */ 124 | error(message) { 125 | this.#log(message, LogLevel.Error); 126 | } 127 | 128 | /** 129 | * @param {string} message 130 | */ 131 | critical(message) { 132 | this.#log(message, LogLevel.Critical); 133 | } 134 | 135 | /** Getters */ 136 | 137 | /** 138 | * @returns {LogLevel} The current log level. 139 | */ 140 | get level() { 141 | return this.#config.level; 142 | } 143 | 144 | /** 145 | * @returns {string} The log file prefix 146 | */ 147 | get file_prefix() { 148 | return this.#config.file_prefix; 149 | } 150 | 151 | /** 152 | * @returns {RollingTimeOptions} 153 | */ 154 | get time_threshold() { 155 | return this.#config.rolling_config.time_threshold; 156 | } 157 | 158 | /** 159 | * @returns {RollingSizeOptions} 160 | */ 161 | get size_threshold() { 162 | return this.#config.rolling_config.size_threshold; 163 | } 164 | } 165 | 166 | module.exports = { Logger }; 167 | -------------------------------------------------------------------------------- /src/chapter_04.5/lib/logtar.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Logger: require('./logger').Logger, 3 | LogConfig: require('./config/log-config').LogConfig, 4 | RollingConfig: require('./config/rolling-config').RollingConfig, 5 | LogLevel: require('./utils/log-level').LogLevel, 6 | RollingTimeOptions: require('./utils/rolling-options').RollingTimeOptions, 7 | RollingSizeOptions: require('./utils/rolling-options').RollingSizeOptions, 8 | }; -------------------------------------------------------------------------------- /src/chapter_04.5/lib/utils/helpers.js: -------------------------------------------------------------------------------- 1 | const fs_sync = require('node:fs'); 2 | const path = require('path') 3 | 4 | /** 5 | * @returns {fs_sync.PathLike} The path to the directory. 6 | */ 7 | function check_and_create_dir(path_to_dir) { 8 | const log_dir = path.resolve(require.main.path, path_to_dir); 9 | if (!fs_sync.existsSync(log_dir)) { 10 | fs_sync.mkdirSync(log_dir, { recursive: true }); 11 | } 12 | 13 | return log_dir 14 | } 15 | 16 | /** 17 | * @returns {string} The meta data of the caller by parsing the stack trace. 18 | */ 19 | function get_caller_info() { 20 | const error = {}; 21 | Error.captureStackTrace(error); 22 | 23 | const caller_frame = error.stack.split("\n")[5]; 24 | 25 | const meta_data = caller_frame.split("at ").pop(); 26 | return meta_data 27 | } 28 | 29 | module.exports = { 30 | check_and_create_dir, 31 | get_caller_info 32 | } -------------------------------------------------------------------------------- /src/chapter_04.5/lib/utils/log-level.js: -------------------------------------------------------------------------------- 1 | class LogLevel { 2 | static #Debug = 0; 3 | static #Info = 1; 4 | static #Warn = 2; 5 | static #Error = 3; 6 | static #Critical = 4; 7 | 8 | /** 9 | * @param {number} log_level 10 | */ 11 | static to_string(log_level) { 12 | const levelMap = { 13 | [this.Debug]: "DEBUG", 14 | [this.Info]: "INFO", 15 | [this.Warn]: "WARN", 16 | [this.Error]: "ERROR", 17 | [this.Critical]: "CRITICAL" 18 | }; 19 | 20 | if (levelMap.hasOwnProperty(log_level)) { 21 | return levelMap[log_level]; 22 | } 23 | 24 | throw new Error(`Unsupported log level ${log_level}`); 25 | } 26 | 27 | static get Debug() { 28 | return this.#Debug; 29 | } 30 | 31 | static get Info() { 32 | return this.#Info; 33 | } 34 | 35 | static get Warn() { 36 | return this.#Warn; 37 | } 38 | 39 | static get Error() { 40 | return this.#Error; 41 | } 42 | 43 | static get Critical() { 44 | return this.#Critical; 45 | } 46 | 47 | static assert(log_level) { 48 | if (![this.Debug, this.Info, this.Warn, this.Error, this.Critical].includes(log_level)) { 49 | throw new Error( 50 | `log_level must be an instance of LogLevel. Unsupported param ${JSON.stringify(log_level)}` 51 | ); 52 | } 53 | } 54 | } 55 | 56 | module.exports = { LogLevel }; 57 | -------------------------------------------------------------------------------- /src/chapter_04.5/lib/utils/rolling-options.js: -------------------------------------------------------------------------------- 1 | class RollingSizeOptions { 2 | static OneKB = 1024; 3 | static FiveKB = 5 * 1024; 4 | static TenKB = 10 * 1024; 5 | static TwentyKB = 20 * 1024; 6 | static FiftyKB = 50 * 1024; 7 | static HundredKB = 100 * 1024; 8 | 9 | static HalfMB = 512 * 1024; 10 | static OneMB = 1024 * 1024; 11 | static FiveMB = 5 * 1024 * 1024; 12 | static TenMB = 10 * 1024 * 1024; 13 | static TwentyMB = 20 * 1024 * 1024; 14 | static FiftyMB = 50 * 1024 * 1024; 15 | static HundredMB = 100 * 1024 * 1024; 16 | 17 | static assert(size_threshold) { 18 | if (typeof size_threshold !== "number" || size_threshold < RollingSizeOptions.OneKB) { 19 | throw new Error( 20 | `size_threshold must be at-least 1 KB. Unsupported param ${JSON.stringify(size_threshold)}` 21 | ); 22 | } 23 | } 24 | } 25 | 26 | class RollingTimeOptions { 27 | static Minutely = 60; // Every 60 seconds 28 | static Hourly = 60 * this.Minutely; 29 | static Daily = 24 * this.Hourly; 30 | static Weekly = 7 * this.Daily; 31 | static Monthly = 30 * this.Daily; 32 | static Yearly = 12 * this.Monthly; 33 | 34 | static assert(time_option) { 35 | if (![this.Minutely, this.Hourly, this.Daily, this.Weekly, this.Monthly, this.Yearly].includes(time_option)) { 36 | throw new Error( 37 | `time_option must be an instance of RollingConfig. Unsupported param ${JSON.stringify(time_option)}` 38 | ); 39 | } 40 | } 41 | } 42 | 43 | module.exports = { 44 | RollingSizeOptions, 45 | RollingTimeOptions, 46 | }; 47 | -------------------------------------------------------------------------------- /src/chapter_04.5/logs/Logtar_2023-08-22T00-42-08.log: -------------------------------------------------------------------------------- 1 | [2023-08-22T00:42:08.091Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 2 | [2023-08-22T00:42:08.103Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 3 | [2023-08-22T00:42:08.114Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 4 | [2023-08-22T00:42:08.124Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 5 | [2023-08-22T00:42:08.136Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 6 | [2023-08-22T00:42:08.148Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 7 | [2023-08-22T00:42:08.158Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 8 | [2023-08-22T00:42:08.169Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 9 | [2023-08-22T00:42:08.181Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 10 | [2023-08-22T00:42:08.193Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 11 | [2023-08-22T00:42:08.203Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 12 | [2023-08-22T00:42:08.214Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 13 | [2023-08-22T00:42:08.225Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 14 | [2023-08-22T00:42:08.236Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 15 | [2023-08-22T00:42:08.246Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 16 | [2023-08-22T00:42:08.257Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 17 | [2023-08-22T00:42:08.268Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 18 | [2023-08-22T00:42:08.280Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 19 | [2023-08-22T00:42:08.290Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 20 | [2023-08-22T00:42:08.301Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 21 | [2023-08-22T00:42:08.313Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 22 | [2023-08-22T00:42:08.324Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 23 | [2023-08-22T00:42:08.334Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 24 | [2023-08-22T00:42:08.346Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 25 | [2023-08-22T00:42:08.356Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 26 | [2023-08-22T00:42:08.368Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 27 | [2023-08-22T00:42:08.378Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 28 | [2023-08-22T00:42:08.389Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 29 | [2023-08-22T00:42:08.400Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 30 | [2023-08-22T00:42:08.411Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 31 | [2023-08-22T00:42:08.422Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 32 | [2023-08-22T00:42:08.434Z] [CRITICAL]: Timeout._onTimeout (/Users/ishtmeet/Code/logtard/test.js:14:16) Hi there 33 | -------------------------------------------------------------------------------- /src/chapter_04.5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logtar", 3 | "version": "0.0.8", 4 | "description": "A lightweight logger for your applications with rolling file support and much more.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "logger", 11 | "fast", 12 | "stream", 13 | "rolling", 14 | "json" 15 | ], 16 | "author": "Ishtmeet Singh ", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/ishtms/logtar.git" 20 | }, 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ishtms/logtar/issues" 24 | }, 25 | "homepage": "https://github.com/ishtms/logtar" 26 | } 27 | -------------------------------------------------------------------------------- /src/chapter_06.01/index.js: -------------------------------------------------------------------------------- 1 | const http = require("node:http"); 2 | 3 | const PORT = 5255; 4 | 5 | const server = http.createServer((request, response) => { 6 | const { headers, data, statusCode } = handleRequest(request); 7 | response.writeHead(statusCode, headers); 8 | response.end(data); 9 | }); 10 | 11 | // The header that needs to be sent on every response. 12 | const baseHeader = { 13 | "Content-Type": "text/plain", 14 | }; 15 | 16 | const routeHandlers = { 17 | "GET /": () => ({ statusCode: 200, data: "Hello World!", headers: { "My-Header": "Hello World!" } }), 18 | "POST /echo": () => ({ statusCode: 201, data: "Yellow World!", headers: { "My-Header": "Yellow World!" } }), 19 | }; 20 | 21 | const handleRequest = ({ method, url }) => { 22 | const handler = 23 | routeHandlers[`${method} ${url}`] || 24 | (() => ({ statusCode: 404, data: "Not Found", headers: { "My-Header": "Not Found" } })); 25 | 26 | const { statusCode, data } = handler(); 27 | const headers = { ...baseHeader, "Content-Length": Buffer.byteLength(data) }; 28 | 29 | return { headers, statusCode, data }; 30 | }; 31 | 32 | server.listen(PORT, () => { 33 | console.log(`Server is listening at :${PORT}`); 34 | }); 35 | -------------------------------------------------------------------------------- /src/chapter_06.02/index.js: -------------------------------------------------------------------------------- 1 | const http = require("node:http"); 2 | 3 | const PORT = 5255; 4 | 5 | class Router { 6 | constructor() { 7 | this.routes = {}; 8 | } 9 | 10 | addRoute(method, path, handler) { 11 | this.routes[`${method} ${path}`] = handler; 12 | } 13 | 14 | handleRequest(request, response) { 15 | const { url, method } = request; 16 | const handler = this.routes[`${method} ${url}`]; 17 | 18 | if (!handler) { 19 | return console.log("404 Not found"); 20 | } 21 | 22 | handler(request, response); 23 | } 24 | 25 | printRoutes() { 26 | console.log(Object.entries(this.routes)); 27 | } 28 | } 29 | 30 | const router = new Router(); 31 | router.addRoute("GET", "/", function handleGetBasePath(req, res) { 32 | console.log("Hello from GET /"); 33 | res.end(); 34 | }); 35 | 36 | router.addRoute("POST", "/", function handlePostBasePath(req, res) { 37 | console.log("Hello from POST /"); 38 | res.end(); 39 | }); 40 | 41 | // Note: We're using an arrow function instead of a regular function now 42 | let server = http.createServer((req, res) => router.handleRequest(req, res)); 43 | server.listen(PORT); 44 | -------------------------------------------------------------------------------- /src/chapter_06.03/index.js: -------------------------------------------------------------------------------- 1 | const http = require("node:http"); 2 | 3 | const PORT = 5255; 4 | 5 | const HTTP_METHODS = { 6 | GET: "GET", 7 | POST: "POST", 8 | PUT: "PUT", 9 | DELETE: "DELETE", 10 | PATCH: "PATCH", 11 | HEAD: "HEAD", 12 | OPTIONS: "OPTIONS", 13 | CONNECT: "CONNECT", 14 | TRACE: "TRACE", 15 | }; 16 | 17 | class Router { 18 | constructor() { 19 | this.routes = {}; 20 | } 21 | 22 | #addRoute(method, path, handler) { 23 | if (typeof path !== "string" || typeof handler !== "function") { 24 | throw new Error("Invalid argument types: path must be a string and handler must be a function"); 25 | } 26 | this.routes[`${method} ${path}`] = handler; 27 | } 28 | 29 | handleRequest(request, response) { 30 | const { url, method } = request; 31 | const handler = this.routes[`${method} ${url}`]; 32 | 33 | if (!handler) { 34 | return console.log("404 Not found"); 35 | } 36 | 37 | handler(request, response); 38 | } 39 | 40 | get(path, handler) { 41 | this.#addRoute(HTTP_METHODS.GET, path, handler); 42 | } 43 | 44 | post(path, handler) { 45 | this.#addRoute(HTTP_METHODS.POST, path, handler); 46 | } 47 | 48 | put(path, handler) { 49 | this.#addRoute(HTTP_METHODS.PUT, path, handler); 50 | } 51 | 52 | delete(path, handler) { 53 | this.#addRoute(HTTP_METHODS.DELETE, path, handler); 54 | } 55 | 56 | patch(path, handler) { 57 | this.#addRoute(HTTP_METHODS.PATCH, path, handler); 58 | } 59 | 60 | head(path, handler) { 61 | this.#addRoute(HTTP_METHODS.HEAD, path, handler); 62 | } 63 | 64 | options(path, handler) { 65 | this.#addRoute(HTTP_METHODS.OPTIONS, path, handler); 66 | } 67 | 68 | connect(path, handler) { 69 | this.#addRoute(HTTP_METHODS.CONNECT, path, handler); 70 | } 71 | 72 | trace(path, handler) { 73 | this.#addRoute(HTTP_METHODS.TRACE, path, handler); 74 | } 75 | 76 | printRoutes() { 77 | console.log(Object.entries(this.routes)); 78 | } 79 | } 80 | 81 | const router = new Router(); 82 | 83 | router.get("/", function handleGetBasePath(req, res) { 84 | console.log("Hello from GET /"); 85 | res.end(); 86 | }); 87 | 88 | router.post("/", function handlePostBasePath(req, res) { 89 | console.log("Hello from POST /"); 90 | res.end(); 91 | }); 92 | 93 | // Note: We're using an arrow function instead of a regular function now 94 | let server = http.createServer((req, res) => router.handleRequest(req, res)); 95 | server.listen(PORT); 96 | -------------------------------------------------------------------------------- /src/chapter_06.05/challenge_1.js: -------------------------------------------------------------------------------- 1 | class TrieNode { 2 | isEndOfWord = false; 3 | children = new Map(); 4 | } 5 | 6 | class Trie { 7 | constructor() { 8 | this.root = new TrieNode(); 9 | } 10 | 11 | insert(word, node = this.root) { 12 | const wordLength = word.length; 13 | 14 | // Exit condition: If the word to insert is empty, terminate the recursion. 15 | if (wordLength === 0) return; 16 | 17 | for (let idx = 0; idx < wordLength; idx++) { 18 | let char = word[idx]; 19 | 20 | // Check if the current node has a child node for the current character. 21 | if (!node.children.has(char)) { 22 | // If not, create a new TrieNode for this character and add it to the children of the current node. 23 | node.children.set(char, new TrieNode()); 24 | } 25 | 26 | // Move to the child node corresponding to the current character. 27 | node = node.children.get(char); 28 | } 29 | 30 | node.isEndOfWord = true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/chapter_06.05/challenge_2.js: -------------------------------------------------------------------------------- 1 | class TrieNode { 2 | isEndOfWord = false; 3 | children = new Map(); 4 | } 5 | 6 | class Trie { 7 | constructor() { 8 | this.root = new TrieNode(); 9 | } 10 | 11 | search(word) { 12 | // Initialize 'currentNode' to the root node of the Trie. 13 | let currentNode = this.root; 14 | 15 | // Loop through each character in the input word. 16 | for (let index = 0; index < word.length; index++) { 17 | // Check if the current character exists as a child node 18 | // of the 'currentNode'. 19 | if (currentNode.children.has(word[index])) { 20 | // If it does, update 'currentNode' to this child node. 21 | currentNode = currentNode.children.get(word[index]); 22 | } else { 23 | // If it doesn't, the word is not in the Trie. Return false. 24 | return false; 25 | } 26 | } 27 | 28 | // After looping through all the characters, check if the 'currentNode' 29 | // marks the end of a word in the Trie. 30 | return currentNode.isEndOfWord; 31 | } 32 | 33 | insert(word, node = this.root) { 34 | const wordLength = word.length; 35 | 36 | if (wordLength === 0) return; 37 | 38 | for (let idx = 0; idx < wordLength; idx++) { 39 | let char = word[idx]; 40 | 41 | if (!node.children.has(char)) { 42 | node.children.set(char, new TrieNode()); 43 | } 44 | 45 | node = node.children.get(char); 46 | } 47 | 48 | node.isEndOfWord = true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/chapter_06.06/challenge_1.js: -------------------------------------------------------------------------------- 1 | class RouteNode { 2 | constructor() { 3 | // Create a Map to store children nodes 4 | this.children = new Map(); 5 | 6 | // Initialize the handler to null 7 | this.handler = null; 8 | } 9 | } 10 | 11 | class TrieRouter { 12 | constructor() { 13 | // Create a root node upon TrieRouter instantiation 14 | this.root = new RouteNode(); 15 | } 16 | 17 | addRoute(path, handler) { 18 | // Validate input types 19 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 20 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 21 | 22 | // Start at the root node of the Trie 23 | let currentNode = this.root; 24 | // Split the path into segments and filter out empty segments 25 | let routeParts = path.split("/").filter(Boolean); 26 | 27 | // Loop through all segments of the route 28 | for (let idx = 0; idx < routeParts.length; idx++) { 29 | const segment = routeParts[idx].toLowerCase(); 30 | if (segment.includes(" ")) throw new Error("Malformed path parameter"); 31 | 32 | // Attempt to find the next node in the Trie 33 | let childNode = currentNode.children.get(segment); 34 | 35 | // If the next node doesn't exist, create it 36 | if (!childNode) { 37 | childNode = new RouteNode(); 38 | currentNode.children.set(segment, childNode); 39 | } 40 | 41 | // Move to the next node for the next iteration 42 | currentNode = childNode; 43 | } 44 | 45 | // Assign the handler to the last node 46 | currentNode.handler = handler; 47 | } 48 | 49 | printTree(node = this.root, indentation = 0) { 50 | const indent = "-".repeat(indentation); 51 | 52 | node.children.forEach((childNode, segment) => { 53 | console.log(`${indent}${segment}`); 54 | this.printTree(childNode, indentation + 1); 55 | }); 56 | } 57 | } 58 | 59 | const trieRouter = new TrieRouter(); 60 | 61 | function ref() {} 62 | 63 | trieRouter.addRoute("/home/", ref); 64 | trieRouter.addRoute("/user/status/play", function inline() {}); 65 | 66 | trieRouter.printTree(); 67 | -------------------------------------------------------------------------------- /src/chapter_06.06/challenge_2.js: -------------------------------------------------------------------------------- 1 | class RouteNode { 2 | constructor() { 3 | this.children = new Map(); 4 | 5 | this.handler = null; 6 | } 7 | } 8 | 9 | class TrieRouter { 10 | constructor() { 11 | this.root = new RouteNode(); 12 | } 13 | 14 | findRoute(path) { 15 | // Split the path into segments and filter out empty segments 16 | let segments = path.split("/").filter(Boolean); 17 | // Start at the root node of the Trie 18 | let currentNode = this.root; 19 | 20 | // Start at the root node of the Trie 21 | for (let idx = 0; idx < segments.length; idx++) { 22 | // Retrieve the current URL segment 23 | const segment = segments[idx]; 24 | 25 | // Retrieve the child node corresponding to the current URL segment 26 | let childNode = currentNode.children.get(segment); 27 | 28 | // If the next node exists, update the current node 29 | if (childNode) { 30 | currentNode = childNode; 31 | } else { 32 | // If the next node exists, update the current node 33 | return null; 34 | } 35 | } 36 | 37 | // If the next node exists, update the current node 38 | return currentNode.handler; 39 | } 40 | 41 | addRoute(path, handler) { 42 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 43 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 44 | 45 | let currentNode = this.root; 46 | let routeParts = path.split("/").filter(Boolean); 47 | 48 | for (let idx = 0; idx < routeParts.length; idx++) { 49 | const segment = routeParts[idx].toLowerCase(); 50 | if (segment.includes(" ")) throw new Error("Malformed path parameter"); 51 | 52 | let childNode = currentNode.children.get(segment); 53 | 54 | if (!childNode) { 55 | childNode = new RouteNode(); 56 | currentNode.children.set(segment, childNode); 57 | } 58 | 59 | currentNode = childNode; 60 | } 61 | 62 | currentNode.handler = handler; 63 | } 64 | 65 | printTree(node = this.root, indentation = 0) { 66 | const indent = "-".repeat(indentation); 67 | 68 | node.children.forEach((childNode, segment) => { 69 | console.log(`${indent}${segment}`); 70 | this.printTree(childNode, indentation + 1); 71 | }); 72 | } 73 | } 74 | 75 | const trieRouter = new TrieRouter(); 76 | 77 | function ref() {} 78 | 79 | trieRouter.addRoute("/home/", ref); 80 | trieRouter.addRoute("/user/status/play", function inline() {}); 81 | 82 | trieRouter.printTree(); 83 | -------------------------------------------------------------------------------- /src/chapter_06.07/index.js: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = { 2 | GET: "GET", 3 | POST: "POST", 4 | PUT: "PUT", 5 | DELETE: "DELETE", 6 | PATCH: "PATCH", 7 | HEAD: "HEAD", 8 | OPTIONS: "OPTIONS", 9 | CONNECT: "CONNECT", 10 | TRACE: "TRACE", 11 | }; 12 | 13 | class RouteNode { 14 | constructor() { 15 | this.children = new Map(); 16 | this.handler = new Map(); 17 | } 18 | } 19 | 20 | class TrieRouter { 21 | constructor() { 22 | this.root = new RouteNode(); 23 | } 24 | 25 | addRoute(path, method, handler) { 26 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 27 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 28 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 29 | 30 | let currentNode = this.root; 31 | let routeParts = path.split("/").filter(Boolean); 32 | 33 | for (let idx = 0; idx < routeParts.length; idx++) { 34 | const segment = routeParts[idx].toLowerCase(); 35 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 36 | 37 | let childNode = currentNode.children.get(segment); 38 | if (!childNode) { 39 | childNode = new RouteNode(); 40 | currentNode.children.set(segment, childNode); 41 | } 42 | 43 | currentNode = childNode; 44 | } 45 | currentNode.handler.set(method, handler); // Changed this line 46 | } 47 | 48 | findRoute(path, method) { 49 | let segments = path.split("/").filter(Boolean); 50 | let currentNode = this.root; 51 | 52 | for (let idx = 0; idx < segments.length; idx++) { 53 | const segment = segments[idx]; 54 | 55 | let childNode = currentNode.children.get(segment); 56 | if (childNode) { 57 | currentNode = childNode; 58 | } else { 59 | return null; 60 | } 61 | } 62 | 63 | return currentNode.handler.get(method); // Changed this line 64 | } 65 | 66 | printTree(node = this.root, indentation = 0) { 67 | const indent = "-".repeat(indentation); 68 | 69 | node.children.forEach((childNode, segment) => { 70 | console.log(`${indent}${segment}`); 71 | this.printTree(childNode, indentation + 1); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/chapter_06.08/index.js: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = { 2 | GET: "GET", 3 | POST: "POST", 4 | PUT: "PUT", 5 | DELETE: "DELETE", 6 | PATCH: "PATCH", 7 | HEAD: "HEAD", 8 | OPTIONS: "OPTIONS", 9 | CONNECT: "CONNECT", 10 | TRACE: "TRACE", 11 | }; 12 | 13 | class RouteNode { 14 | constructor() { 15 | this.children = new Map(); 16 | this.handler = new Map(); 17 | } 18 | } 19 | 20 | class TrieRouter { 21 | constructor() { 22 | this.root = new RouteNode(); 23 | } 24 | 25 | #addRoute(path, method, handler) { 26 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 27 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 28 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 29 | 30 | let currentNode = this.root; 31 | let routeParts = path.split("/").filter(Boolean); 32 | 33 | for (let idx = 0; idx < routeParts.length; idx++) { 34 | const segment = routeParts[idx].toLowerCase(); 35 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 36 | 37 | let childNode = currentNode.children.get(segment); 38 | if (!childNode) { 39 | childNode = new RouteNode(); 40 | currentNode.children.set(segment, childNode); 41 | } 42 | 43 | currentNode = childNode; 44 | } 45 | currentNode.handler.set(method, handler); // Changed this line 46 | } 47 | 48 | findRoute(path, method) { 49 | let segments = path.split("/").filter(Boolean); 50 | let currentNode = this.root; 51 | 52 | for (let idx = 0; idx < segments.length; idx++) { 53 | const segment = segments[idx]; 54 | 55 | let childNode = currentNode.children.get(segment); 56 | if (childNode) { 57 | currentNode = childNode; 58 | } else { 59 | return null; 60 | } 61 | } 62 | 63 | return currentNode.handler.get(method); // Changed this line 64 | } 65 | 66 | printTree(node = this.root, indentation = 0) { 67 | const indent = "-".repeat(indentation); 68 | 69 | node.children.forEach((childNode, segment) => { 70 | console.log(`${indent}${segment}`); 71 | this.printTree(childNode, indentation + 1); 72 | }); 73 | } 74 | get(path, handler) { 75 | this.#addRoute(path, HTTP_METHODS.GET, handler); 76 | } 77 | 78 | post(path, handler) { 79 | this.#addRoute(path, HTTP_METHODS.POST, handler); 80 | } 81 | 82 | put(path, handler) { 83 | this.#addRoute(path, HTTP_METHODS.PUT, handler); 84 | } 85 | 86 | delete(path, handler) { 87 | this.#addRoute(path, HTTP_METHODS.DELETE, handler); 88 | } 89 | 90 | patch(path, handler) { 91 | this.#addRoute(path, HTTP_METHODS.PATCH, handler); 92 | } 93 | 94 | head(path, handler) { 95 | this.#addRoute(path, HTTP_METHODS.HEAD, handler); 96 | } 97 | 98 | options(path, handler) { 99 | this.#addRoute(path, HTTP_METHODS.OPTIONS, handler); 100 | } 101 | 102 | connect(path, handler) { 103 | this.#addRoute(path, HTTP_METHODS.CONNECT, handler); 104 | } 105 | 106 | trace(path, handler) { 107 | this.#addRoute(path, HTTP_METHODS.TRACE, handler); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/chapter_06.09/index.js: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = { 2 | GET: "GET", 3 | POST: "POST", 4 | PUT: "PUT", 5 | DELETE: "DELETE", 6 | PATCH: "PATCH", 7 | HEAD: "HEAD", 8 | OPTIONS: "OPTIONS", 9 | CONNECT: "CONNECT", 10 | TRACE: "TRACE", 11 | }; 12 | 13 | class RouteNode { 14 | constructor() { 15 | this.children = new Map(); 16 | this.handler = new Map(); 17 | this.params = []; 18 | } 19 | } 20 | 21 | class TrieRouter { 22 | constructor() { 23 | this.root = new RouteNode(); 24 | } 25 | 26 | #verifyParams(path, method, handler) { 27 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 28 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 29 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 30 | } 31 | 32 | #addRoute(path, method, handler) { 33 | this.#verifyParams(path, method, handler); 34 | 35 | let currentNode = this.root; 36 | let routeParts = path.split("/").filter(Boolean); 37 | let dynamicParams = []; 38 | 39 | for (const segment of routeParts) { 40 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 41 | 42 | const isDynamic = segment[0] === ":"; 43 | const key = isDynamic ? ":" : segment.toLowerCase(); 44 | 45 | if (isDynamic) { 46 | dynamicParams.push(segment.substring(1)); 47 | } 48 | 49 | if (!currentNode.children.has(key)) { 50 | currentNode.children.set(key, new RouteNode()); 51 | } 52 | 53 | currentNode = currentNode.children.get(key); 54 | } 55 | 56 | currentNode.handler.set(method, handler); 57 | currentNode.params = dynamicParams; 58 | } 59 | 60 | findRoute(path, method) { 61 | let segments = path.split("/").filter(Boolean); 62 | let currentNode = this.root; 63 | let extractedParams = []; 64 | 65 | for (let idx = 0; idx < segments.length; idx++) { 66 | const segment = segments[idx]; 67 | 68 | let childNode = currentNode.children.get(segment.toLowerCase()); 69 | if (childNode) { 70 | currentNode = childNode; 71 | } else if ((childNode = currentNode.children.get(":"))) { 72 | extractedParams.push(segment); 73 | currentNode = childNode; 74 | } else { 75 | return null; 76 | } 77 | } 78 | 79 | let params = Object.create(null); 80 | 81 | for (let idx = 0; idx < extractedParams.length; idx++) { 82 | let key = currentNode.params[idx]; 83 | let value = extractedParams[idx]; 84 | 85 | params[key] = value; 86 | } 87 | 88 | return { 89 | params, 90 | handler: currentNode.handler.get(method), 91 | }; 92 | } 93 | 94 | get(path, handler) { 95 | this.#addRoute(path, HTTP_METHODS.GET, handler); 96 | } 97 | 98 | post(path, handler) { 99 | this.#addRoute(path, HTTP_METHODS.POST, handler); 100 | } 101 | 102 | put(path, handler) { 103 | this.#addRoute(path, HTTP_METHODS.PUT, handler); 104 | } 105 | 106 | delete(path, handler) { 107 | this.#addRoute(path, HTTP_METHODS.DELETE, handler); 108 | } 109 | 110 | patch(path, handler) { 111 | this.#addRoute(path, HTTP_METHODS.PATCH, handler); 112 | } 113 | 114 | head(path, handler) { 115 | this.#addRoute(path, HTTP_METHODS.HEAD, handler); 116 | } 117 | 118 | options(path, handler) { 119 | this.#addRoute(path, HTTP_METHODS.OPTIONS, handler); 120 | } 121 | 122 | connect(path, handler) { 123 | this.#addRoute(path, HTTP_METHODS.CONNECT, handler); 124 | } 125 | 126 | trace(path, handler) { 127 | this.#addRoute(path, HTTP_METHODS.TRACE, handler); 128 | } 129 | 130 | printTree(node = this.root, indentation = 0) { 131 | const indent = "-".repeat(indentation); 132 | 133 | node.children.forEach((childNode, segment) => { 134 | console.log(`${indent}(${segment}) Dynamic: ${childNode.params}`); 135 | this.printTree(childNode, indentation + 1); 136 | }); 137 | } 138 | } 139 | 140 | const trieRouter = new TrieRouter(); 141 | trieRouter.get("/users/:id/hello/there/:some/:hello", function get1() {}); 142 | trieRouter.post("/users/:some/hello/there/:id/none", function post1() {}); 143 | 144 | console.log("Printing Tree:"); 145 | trieRouter.printTree(); 146 | 147 | console.log("Finding Handlers:"); 148 | 149 | console.log(trieRouter.findRoute("/users/e/hello/there/2/3", HTTP_METHODS.GET)); 150 | console.log(trieRouter.findRoute("/users/1/hello/there/2/none", HTTP_METHODS.GET)); 151 | console.log(trieRouter.findRoute("/users", HTTP_METHODS.PUT)); 152 | console.log(trieRouter.findRoute("/users", HTTP_METHODS.TRACE)); 153 | -------------------------------------------------------------------------------- /src/chapter_06.10/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod 3 | */ 4 | -------------------------------------------------------------------------------- /src/chapter_06.10/index.js: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = { 2 | GET: "GET", 3 | POST: "POST", 4 | PUT: "PUT", 5 | DELETE: "DELETE", 6 | PATCH: "PATCH", 7 | HEAD: "HEAD", 8 | OPTIONS: "OPTIONS", 9 | CONNECT: "CONNECT", 10 | TRACE: "TRACE", 11 | }; 12 | 13 | class RouteNode { 14 | constructor() { 15 | /** @type {Map} */ 16 | this.children = new Map(); 17 | 18 | /** @type {Map} */ 19 | this.handler = new Map(); 20 | 21 | /** @type {Array} */ 22 | this.params = []; 23 | } 24 | } 25 | 26 | class Router { 27 | constructor() { 28 | /** @type {RouteNode} */ 29 | this.root = new RouteNode(); 30 | } 31 | 32 | /** 33 | * @param {String} path 34 | * @param {HttpMethod} method 35 | * @param {Function} handler 36 | */ 37 | #verifyParams(path, method, handler) { 38 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 39 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 40 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 41 | } 42 | 43 | /** 44 | * @param {String} path 45 | * @param {HttpMethod } method 46 | * @param {Function} handler 47 | */ 48 | #addRoute(path, method, handler) { 49 | this.#verifyParams(path, method, handler); 50 | 51 | let currentNode = this.root; 52 | let routeParts = path.split("/").filter(Boolean); 53 | let dynamicParams = []; 54 | 55 | for (const segment of routeParts) { 56 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 57 | 58 | const isDynamic = segment[0] === ":"; 59 | const key = isDynamic ? ":" : segment.toLowerCase(); 60 | 61 | if (isDynamic) { 62 | dynamicParams.push(segment.substring(1)); 63 | } 64 | 65 | if (!currentNode.children.has(key)) { 66 | currentNode.children.set(key, new RouteNode()); 67 | } 68 | 69 | currentNode = currentNode.children.get(key); 70 | } 71 | 72 | currentNode.handler.set(method, handler); 73 | currentNode.params = dynamicParams; 74 | } 75 | 76 | /** 77 | * @param {String} path 78 | * @param {HttpMethod} method 79 | * @returns { { params: Object, handler: Function } | null } 80 | */ 81 | findRoute(path, method) { 82 | let segments = path.split("/").filter(Boolean); 83 | let currentNode = this.root; 84 | let extractedParams = []; 85 | 86 | for (let idx = 0; idx < segments.length; idx++) { 87 | const segment = segments[idx]; 88 | 89 | let childNode = currentNode.children.get(segment.toLowerCase()); 90 | if (childNode) { 91 | currentNode = childNode; 92 | } else if ((childNode = currentNode.children.get(":"))) { 93 | extractedParams.push(segment); 94 | currentNode = childNode; 95 | } else { 96 | return null; 97 | } 98 | } 99 | 100 | let params = Object.create(null); 101 | 102 | for (let idx = 0; idx < extractedParams.length; idx++) { 103 | let key = currentNode.params[idx]; 104 | let value = extractedParams[idx]; 105 | 106 | params[key] = value; 107 | } 108 | 109 | return { 110 | params, 111 | handler: currentNode.handler.get(method), 112 | }; 113 | } 114 | 115 | /** 116 | * @param {String} path 117 | * @param {Function} handler 118 | */ 119 | get(path, handler) { 120 | this.#addRoute(path, HTTP_METHODS.GET, handler); 121 | } 122 | 123 | /** 124 | * @param {String} path 125 | * @param {Function} handler 126 | */ 127 | post(path, handler) { 128 | this.#addRoute(path, HTTP_METHODS.POST, handler); 129 | } 130 | 131 | /** 132 | * @param {String} path 133 | * @param {Function} handler 134 | */ 135 | put(path, handler) { 136 | this.#addRoute(path, HTTP_METHODS.PUT, handler); 137 | } 138 | 139 | /** 140 | * @param {String} path 141 | * @param {Function} handler 142 | */ 143 | delete(path, handler) { 144 | this.#addRoute(path, HTTP_METHODS.DELETE, handler); 145 | } 146 | 147 | /** 148 | * @param {String} path 149 | * @param {Function} handler 150 | */ 151 | patch(path, handler) { 152 | this.#addRoute(path, HTTP_METHODS.PATCH, handler); 153 | } 154 | 155 | /** 156 | * @param {String} path 157 | * @param {Function} handler 158 | */ 159 | head(path, handler) { 160 | this.#addRoute(path, HTTP_METHODS.HEAD, handler); 161 | } 162 | 163 | /** 164 | * @param {String} path 165 | * @param {Function} handler 166 | */ 167 | options(path, handler) { 168 | this.#addRoute(path, HTTP_METHODS.OPTIONS, handler); 169 | } 170 | 171 | /** 172 | * @param {String} path 173 | * @param {Function} handler 174 | */ 175 | connect(path, handler) { 176 | this.#addRoute(path, HTTP_METHODS.CONNECT, handler); 177 | } 178 | 179 | /** 180 | * @param {String} path 181 | * @param {Function} handler 182 | */ 183 | trace(path, handler) { 184 | this.#addRoute(path, HTTP_METHODS.TRACE, handler); 185 | } 186 | 187 | /** 188 | * @param {RouteNode} node 189 | * @param {number} indentation 190 | */ 191 | printTree(node = this.root, indentation = 0) { 192 | const indent = "-".repeat(indentation); 193 | 194 | node.children.forEach((childNode, segment) => { 195 | console.log(`${indent}(${segment}) Dynamic: ${childNode.params}`); 196 | this.printTree(childNode, indentation + 1); 197 | }); 198 | } 199 | } 200 | 201 | const router = new Router(); 202 | 203 | const { createServer } = require("node:http"); 204 | 205 | /** 206 | * Run the server on the specified port 207 | * @param {Router} router - The router to use for routing requests 208 | * @param {number} port - The port to listen on 209 | */ 210 | function run(router, port) { 211 | if (!(router instanceof Router)) { 212 | throw new Error("`router` argument must be an instance of Router"); 213 | } 214 | if (typeof port !== "number") { 215 | throw new Error("`port` argument must be a number"); 216 | } 217 | 218 | createServer(function _create(req, res) { 219 | const route = router.findRoute(req.url, req.path); 220 | 221 | if (route?.handler) { 222 | req.params = route.params || {}; 223 | route.handler(req, res); 224 | } else { 225 | res.writeHead(404, null, { "content-length": 9 }); 226 | res.end("Not Found"); 227 | } 228 | }).listen(port); 229 | } 230 | 231 | run(router, 8000); 232 | -------------------------------------------------------------------------------- /src/chapter_06.11/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod 3 | */ 4 | 5 | /** 6 | * @typedef {import("http").RequestListener} RequestHandler 7 | */ 8 | -------------------------------------------------------------------------------- /src/chapter_06.11/lib/constants.js: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = Object.freeze({ 2 | GET: "GET", 3 | POST: "POST", 4 | PUT: "PUT", 5 | DELETE: "DELETE", 6 | PATCH: "PATCH", 7 | HEAD: "HEAD", 8 | OPTIONS: "OPTIONS", 9 | CONNECT: "CONNECT", 10 | TRACE: "TRACE", 11 | }); 12 | 13 | module.exports = { 14 | HTTP_METHODS, 15 | }; 16 | -------------------------------------------------------------------------------- /src/chapter_06.11/lib/index.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require("node:http"); 2 | const Router = require("./router"); 3 | 4 | /** 5 | * Run the server on the specified port 6 | * @param {Router} router - The router to use for routing requests 7 | * @param {number} port - The port to listen on 8 | */ 9 | function run(router, port) { 10 | if (!(router instanceof Router)) { 11 | throw new Error("`router` argument must be an instance of Router"); 12 | } 13 | if (typeof port !== "number") { 14 | throw new Error("`port` argument must be a number"); 15 | } 16 | 17 | createServer(function _create(req, res) { 18 | const route = router.findRoute(req.url, req.method); 19 | 20 | if (route?.handler) { 21 | req.params = route.params || {}; 22 | route.handler(req, res); 23 | } else { 24 | res.writeHead(404, null, { "content-length": 9 }); 25 | res.end("Not Found"); 26 | } 27 | }).listen(port); 28 | } 29 | 30 | module.exports = { Router, run }; 31 | -------------------------------------------------------------------------------- /src/chapter_06.11/lib/router.js: -------------------------------------------------------------------------------- 1 | const { HTTP_METHODS } = require("./constants"); 2 | 3 | class RouteNode { 4 | constructor() { 5 | /** @type {Map} */ 6 | this.children = new Map(); 7 | 8 | /** @type {Map} */ 9 | this.handler = new Map(); 10 | 11 | /** @type {Array} */ 12 | this.params = []; 13 | } 14 | } 15 | 16 | class Router { 17 | constructor() { 18 | /** @type {RouteNode} */ 19 | this.root = new RouteNode(); 20 | } 21 | 22 | /** 23 | * @param {String} path 24 | * @param {HttpMethod} method 25 | * @param {RequestHandler} handler 26 | */ 27 | #verifyParams(path, method, handler) { 28 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 29 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 30 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 31 | } 32 | 33 | /** 34 | * @param {String} path 35 | * @param {HttpMethod } method 36 | * @param {RequestHandler} handler 37 | */ 38 | #addRoute(path, method, handler) { 39 | this.#verifyParams(path, method, handler); 40 | 41 | let currentNode = this.root; 42 | let routeParts = path.split("/").filter(Boolean); 43 | let dynamicParams = []; 44 | 45 | for (const segment of routeParts) { 46 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 47 | 48 | const isDynamic = segment[0] === ":"; 49 | const key = isDynamic ? ":" : segment.toLowerCase(); 50 | 51 | if (isDynamic) { 52 | dynamicParams.push(segment.substring(1)); 53 | } 54 | 55 | if (!currentNode.children.has(key)) { 56 | currentNode.children.set(key, new RouteNode()); 57 | } 58 | 59 | currentNode = currentNode.children.get(key); 60 | } 61 | 62 | currentNode.handler.set(method, handler); 63 | currentNode.params = dynamicParams; 64 | } 65 | 66 | /** 67 | * @param {String} path 68 | * @param {HttpMethod} method 69 | * @returns { { params: Object, handler: RequestHandler } | null } 70 | */ 71 | findRoute(path, method) { 72 | let segments = path.split("/").filter(Boolean); 73 | let currentNode = this.root; 74 | let extractedParams = []; 75 | 76 | for (let idx = 0; idx < segments.length; idx++) { 77 | const segment = segments[idx]; 78 | 79 | let childNode = currentNode.children.get(segment.toLowerCase()); 80 | if (childNode) { 81 | currentNode = childNode; 82 | } else if ((childNode = currentNode.children.get(":"))) { 83 | extractedParams.push(segment); 84 | currentNode = childNode; 85 | } else { 86 | return null; 87 | } 88 | } 89 | 90 | let params = Object.create(null); 91 | 92 | for (let idx = 0; idx < extractedParams.length; idx++) { 93 | let key = currentNode.params[idx]; 94 | let value = extractedParams[idx]; 95 | 96 | params[key] = value; 97 | } 98 | 99 | return { 100 | params, 101 | handler: currentNode.handler.get(method), 102 | }; 103 | } 104 | 105 | /** 106 | * @param {String} path 107 | * @param {RequestHandler} handler 108 | */ 109 | get(path, handler) { 110 | this.#addRoute(path, HTTP_METHODS.GET, handler); 111 | } 112 | 113 | /** 114 | * @param {String} path 115 | * @param {RequestHandler} handler 116 | */ 117 | post(path, handler) { 118 | this.#addRoute(path, HTTP_METHODS.POST, handler); 119 | } 120 | 121 | /** 122 | * @param {String} path 123 | * @param {RequestHandler} handler 124 | */ 125 | put(path, handler) { 126 | this.#addRoute(path, HTTP_METHODS.PUT, handler); 127 | } 128 | 129 | /** 130 | * @param {String} path 131 | * @param {RequestHandler} handler 132 | */ 133 | delete(path, handler) { 134 | this.#addRoute(path, HTTP_METHODS.DELETE, handler); 135 | } 136 | 137 | /** 138 | * @param {String} path 139 | * @param {RequestHandler} handler 140 | */ 141 | patch(path, handler) { 142 | this.#addRoute(path, HTTP_METHODS.PATCH, handler); 143 | } 144 | 145 | /** 146 | * @param {String} path 147 | * @param {RequestHandler} handler 148 | */ 149 | head(path, handler) { 150 | this.#addRoute(path, HTTP_METHODS.HEAD, handler); 151 | } 152 | 153 | /** 154 | * @param {String} path 155 | * @param {RequestHandler} handler 156 | */ 157 | options(path, handler) { 158 | this.#addRoute(path, HTTP_METHODS.OPTIONS, handler); 159 | } 160 | 161 | /** 162 | * @param {String} path 163 | * @param {RequestHandler} handler 164 | */ 165 | connect(path, handler) { 166 | this.#addRoute(path, HTTP_METHODS.CONNECT, handler); 167 | } 168 | 169 | /** 170 | * @param {String} path 171 | * @param {RequestHandler} handler 172 | */ 173 | trace(path, handler) { 174 | this.#addRoute(path, HTTP_METHODS.TRACE, handler); 175 | } 176 | 177 | /** 178 | * @param {RouteNode} node 179 | * @param {number} indentation 180 | */ 181 | printTree(node = this.root, indentation = 0) { 182 | const indent = "-".repeat(indentation); 183 | 184 | node.children.forEach((childNode, segment) => { 185 | console.log(`${indent}(${segment}) Dynamic: ${childNode.params}`); 186 | this.printTree(childNode, indentation + 1); 187 | }); 188 | } 189 | } 190 | 191 | module.exports = Router; 192 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge1/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod 3 | */ 4 | 5 | /** 6 | * @typedef {import("http").RequestListener} RequestHandler 7 | */ 8 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge1/lib/constants.js: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = Object.freeze({ 2 | GET: "GET", 3 | POST: "POST", 4 | PUT: "PUT", 5 | DELETE: "DELETE", 6 | PATCH: "PATCH", 7 | HEAD: "HEAD", 8 | OPTIONS: "OPTIONS", 9 | CONNECT: "CONNECT", 10 | TRACE: "TRACE", 11 | }); 12 | 13 | module.exports = { 14 | HTTP_METHODS, 15 | }; 16 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge1/lib/index.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require("node:http"); 2 | const Router = require("./router"); 3 | 4 | /** 5 | * Run the server on the specified port 6 | * @param {Router} router - The router to use for routing requests 7 | * @param {number} port - The port to listen on 8 | */ 9 | function run(router, port) { 10 | if (!(router instanceof Router)) { 11 | throw new Error("`router` argument must be an instance of Router"); 12 | } 13 | if (typeof port !== "number") { 14 | throw new Error("`port` argument must be a number"); 15 | } 16 | 17 | createServer(function _create(req, res) { 18 | const route = router.findRoute(req.url, req.method); 19 | 20 | if (route?.handler) { 21 | req.params = route.params || {}; 22 | req.query = route.query || {}; 23 | route.handler(req, res); 24 | } else { 25 | res.writeHead(404, null, { "content-length": 9 }); 26 | res.end("Not Found"); 27 | } 28 | }).listen(port); 29 | } 30 | 31 | module.exports = { Router, run }; 32 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge1/lib/router.js: -------------------------------------------------------------------------------- 1 | const { HTTP_METHODS } = require("./constants"); 2 | 3 | class RouteNode { 4 | constructor() { 5 | /** @type {Map} */ 6 | this.children = new Map(); 7 | 8 | /** @type {Map} */ 9 | this.handler = new Map(); 10 | /** @type {Array} */ 11 | this.params = []; 12 | } 13 | } 14 | 15 | class Router { 16 | constructor() { 17 | /** @type {RouteNode} */ 18 | this.root = new RouteNode(); 19 | } 20 | 21 | /** 22 | * @param {String} path 23 | * @param {HttpMethod} method 24 | * @param {RequestHandler} handler 25 | */ 26 | #verifyParams(path, method, handler) { 27 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 28 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 29 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 30 | } 31 | 32 | /** 33 | * Parse query parameters from the URL 34 | * @param {string} queryString - URL segment containing query parameters 35 | * @returns {object} - Parsed query parameters as key-value pairs 36 | */ 37 | #parseQueryParams(queryString) { 38 | if (!queryString) return {}; 39 | 40 | const queryParams = {}; 41 | const pairs = queryString.split("&"); 42 | 43 | for (const pair of pairs) { 44 | const [key, value] = pair.split("="); 45 | if (key && value) { 46 | queryParams[key] = decodeURIComponent(value); 47 | } 48 | } 49 | 50 | return queryParams; 51 | } 52 | 53 | /** 54 | * @param {String} path 55 | * @param {HttpMethod } method 56 | * @param {RequestHandler} handler 57 | */ 58 | #addRoute(path, method, handler) { 59 | this.#verifyParams(path, method, handler); 60 | 61 | let currentNode = this.root; 62 | let routeParts = path.split("/").filter(Boolean); 63 | let dynamicParams = []; 64 | 65 | for (const segment of routeParts) { 66 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 67 | 68 | const isDynamic = segment[0] === ":"; 69 | const key = isDynamic ? ":" : segment.toLowerCase(); 70 | 71 | if (isDynamic) { 72 | dynamicParams.push(segment.substring(1)); 73 | } 74 | 75 | if (!currentNode.children.has(key)) { 76 | currentNode.children.set(key, new RouteNode()); 77 | } 78 | 79 | currentNode = currentNode.children.get(key); 80 | } 81 | 82 | currentNode.handler.set(method, handler); 83 | currentNode.params = dynamicParams; 84 | } 85 | 86 | /** 87 | * @param {String} path 88 | * @param {HttpMethod} method 89 | * @returns { { params: Object, handler: RequestHandler } | null } 90 | */ 91 | findRoute(path, method) { 92 | const indexOfDelimiter = path.indexOf("?"); 93 | let _path, querySegment; 94 | 95 | if (indexOfDelimiter !== -1) { 96 | _path = path.substring(0, indexOfDelimiter); 97 | querySegment = path.substring(indexOfDelimiter + 1); 98 | } else { 99 | _path = path; 100 | } 101 | 102 | let segments = _path.split("/").filter(Boolean); 103 | 104 | let currentNode = this.root; 105 | let extractedParams = []; 106 | 107 | for (let idx = 0; idx < segments.length; idx++) { 108 | const segment = segments[idx]; 109 | 110 | let childNode = currentNode.children.get(segment.toLowerCase()); 111 | if (childNode) { 112 | currentNode = childNode; 113 | } else if ((childNode = currentNode.children.get(":"))) { 114 | extractedParams.push(segment); 115 | currentNode = childNode; 116 | } else { 117 | return null; 118 | } 119 | } 120 | 121 | let params = Object.create(null); 122 | 123 | for (let idx = 0; idx < extractedParams.length; idx++) { 124 | let key = currentNode.params[idx]; 125 | let value = extractedParams[idx]; 126 | 127 | params[key] = value; 128 | } 129 | 130 | let query = querySegment ? this.#parseQueryParams(querySegment) : {}; 131 | return { 132 | params, 133 | query, 134 | handler: currentNode.handler.get(method), 135 | }; 136 | } 137 | 138 | /** 139 | * @param {String} path 140 | * @param {RequestHandler} handler 141 | */ 142 | get(path, handler) { 143 | this.#addRoute(path, HTTP_METHODS.GET, handler); 144 | } 145 | 146 | /** 147 | * @param {String} path 148 | * @param {RequestHandler} handler 149 | */ 150 | post(path, handler) { 151 | this.#addRoute(path, HTTP_METHODS.POST, handler); 152 | } 153 | 154 | /** 155 | * @param {String} path 156 | * @param {RequestHandler} handler 157 | */ 158 | put(path, handler) { 159 | this.#addRoute(path, HTTP_METHODS.PUT, handler); 160 | } 161 | 162 | /** 163 | * @param {String} path 164 | * @param {RequestHandler} handler 165 | */ 166 | delete(path, handler) { 167 | this.#addRoute(path, HTTP_METHODS.DELETE, handler); 168 | } 169 | 170 | /** 171 | * @param {String} path 172 | * @param {RequestHandler} handler 173 | */ 174 | patch(path, handler) { 175 | this.#addRoute(path, HTTP_METHODS.PATCH, handler); 176 | } 177 | 178 | /** 179 | * @param {String} path 180 | * @param {RequestHandler} handler 181 | */ 182 | head(path, handler) { 183 | this.#addRoute(path, HTTP_METHODS.HEAD, handler); 184 | } 185 | 186 | /** 187 | * @param {String} path 188 | * @param {RequestHandler} handler 189 | */ 190 | options(path, handler) { 191 | this.#addRoute(path, HTTP_METHODS.OPTIONS, handler); 192 | } 193 | 194 | /** 195 | * @param {String} path 196 | * @param {RequestHandler} handler 197 | */ 198 | connect(path, handler) { 199 | this.#addRoute(path, HTTP_METHODS.CONNECT, handler); 200 | } 201 | 202 | /** 203 | * @param {String} path 204 | * @param {RequestHandler} handler 205 | */ 206 | trace(path, handler) { 207 | this.#addRoute(path, HTTP_METHODS.TRACE, handler); 208 | } 209 | 210 | /** 211 | * @param {RouteNode} node 212 | * @param {number} indentation 213 | */ 214 | printTree(node = this.root, indentation = 0) { 215 | const indent = "-".repeat(indentation); 216 | 217 | node.children.forEach((childNode, segment) => { 218 | console.log(`${indent}(${segment}) Dynamic: ${childNode.params}`); 219 | this.printTree(childNode, indentation + 1); 220 | }); 221 | } 222 | } 223 | 224 | module.exports = Router; 225 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge2/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod 3 | */ 4 | 5 | /** 6 | * @typedef {import("http").RequestListener} RequestHandler 7 | */ 8 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge2/lib/constants.js: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = Object.freeze({ 2 | GET: "GET", 3 | POST: "POST", 4 | PUT: "PUT", 5 | DELETE: "DELETE", 6 | PATCH: "PATCH", 7 | HEAD: "HEAD", 8 | OPTIONS: "OPTIONS", 9 | CONNECT: "CONNECT", 10 | TRACE: "TRACE", 11 | }); 12 | 13 | module.exports = { 14 | HTTP_METHODS, 15 | }; 16 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge2/lib/index.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require("node:http"); 2 | const Router = require("./router"); 3 | 4 | /** 5 | * Run the server on the specified port 6 | * @param {Router} router - The router to use for routing requests 7 | * @param {number} port - The port to listen on 8 | */ 9 | function run(router, port) { 10 | if (!(router instanceof Router)) { 11 | throw new Error("`router` argument must be an instance of Router"); 12 | } 13 | if (typeof port !== "number") { 14 | throw new Error("`port` argument must be a number"); 15 | } 16 | 17 | let server = createServer(function _create(req, res) { 18 | const route = router.findRoute(req.url, req.method); 19 | 20 | if (route?.handler) { 21 | req.params = route.params || {}; 22 | req.query = route.query || new Map(); 23 | route.handler(req, res); 24 | } else { 25 | res.writeHead(404, null, { "content-length": 9 }); 26 | res.end("Not Found"); 27 | } 28 | }).listen(port); 29 | 30 | server.keepAliveTimeout = 72000; 31 | } 32 | 33 | module.exports = { Router, run }; 34 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge2/lib/router.js: -------------------------------------------------------------------------------- 1 | const { HTTP_METHODS } = require("./constants"); 2 | const { fastDecode } = require("./utils"); 3 | 4 | class RouteNode { 5 | constructor() { 6 | /** @type {Map} */ 7 | this.children = new Map(); 8 | 9 | /** @type {Map} */ 10 | this.handler = new Map(); 11 | /** @type {Array} */ 12 | this.params = []; 13 | } 14 | } 15 | 16 | class Router { 17 | constructor() { 18 | /** @type {RouteNode} */ 19 | this.root = new RouteNode(); 20 | } 21 | 22 | /** 23 | * @param {String} path 24 | * @param {HttpMethod} method 25 | * @param {RequestHandler} handler 26 | */ 27 | #verifyParams(path, method, handler) { 28 | if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided."); 29 | if (typeof handler !== "function") throw new Error("Handler should be a function"); 30 | if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method"); 31 | } 32 | 33 | #parseQueryParams(queryString = "") { 34 | if (!queryString) return {}; 35 | 36 | const queryParams = {}; 37 | const pairs = queryString.split("&"); 38 | 39 | for (const pair of pairs) { 40 | const splitPair = pair.split("="); 41 | let key = splitPair[0]; 42 | let value = splitPair[1] || ""; 43 | 44 | if (key.indexOf("%") !== -1) { 45 | key = fastDecode(key); 46 | } 47 | 48 | if (value.indexOf("%") !== -1) { 49 | value = fastDecode(value); 50 | } 51 | 52 | if (key) { 53 | queryParams[key] = value; 54 | } 55 | } 56 | 57 | return queryParams; 58 | } 59 | 60 | /** 61 | * @param {String} path 62 | * @param {HttpMethod } method 63 | * @param {RequestHandler} handler 64 | */ 65 | #addRoute(path, method, handler) { 66 | this.#verifyParams(path, method, handler); 67 | 68 | let currentNode = this.root; 69 | let routeParts = path.split("/").filter(Boolean); 70 | let dynamicParams = []; 71 | 72 | for (const segment of routeParts) { 73 | if (segment.includes(" ")) throw new Error("Malformed `path` parameter"); 74 | 75 | const isDynamic = segment[0] === ":"; 76 | const key = isDynamic ? ":" : segment.toLowerCase(); 77 | 78 | if (isDynamic) { 79 | dynamicParams.push(segment.substring(1)); 80 | } 81 | 82 | if (!currentNode.children.has(key)) { 83 | currentNode.children.set(key, new RouteNode()); 84 | } 85 | 86 | currentNode = currentNode.children.get(key); 87 | } 88 | 89 | currentNode.handler.set(method, handler); 90 | currentNode.params = dynamicParams; 91 | } 92 | 93 | /** 94 | * @param {String} path 95 | * @param {HttpMethod} method 96 | * @returns { { params: Object, handler: RequestHandler } | null } 97 | */ 98 | findRoute(path, method) { 99 | const indexOfDelimiter = path.indexOf("?"); 100 | let _path, querySegment; 101 | 102 | if (indexOfDelimiter !== -1) { 103 | _path = path.substring(0, indexOfDelimiter); 104 | querySegment = path.substring(indexOfDelimiter + 1); 105 | } else { 106 | _path = path; 107 | } 108 | 109 | let segments = _path.split("/").filter(Boolean); 110 | 111 | let currentNode = this.root; 112 | let extractedParams = []; 113 | 114 | for (let idx = 0; idx < segments.length; idx++) { 115 | const segment = segments[idx]; 116 | 117 | let childNode = currentNode.children.get(segment.toLowerCase()); 118 | if (childNode) { 119 | currentNode = childNode; 120 | } else if ((childNode = currentNode.children.get(":"))) { 121 | extractedParams.push(segment); 122 | currentNode = childNode; 123 | } else { 124 | return null; 125 | } 126 | } 127 | 128 | let params = Object.create(null); 129 | 130 | for (let idx = 0; idx < extractedParams.length; idx++) { 131 | let key = currentNode.params[idx]; 132 | let value = extractedParams[idx]; 133 | 134 | params[key] = value; 135 | } 136 | 137 | let query = querySegment ? this.#parseQueryParams(querySegment) : {}; 138 | return { 139 | params, 140 | query, 141 | handler: currentNode.handler.get(method), 142 | }; 143 | } 144 | 145 | /** 146 | * @param {String} path 147 | * @param {RequestHandler} handler 148 | */ 149 | get(path, handler) { 150 | this.#addRoute(path, HTTP_METHODS.GET, handler); 151 | } 152 | 153 | /** 154 | * @param {String} path 155 | * @param {RequestHandler} handler 156 | */ 157 | post(path, handler) { 158 | this.#addRoute(path, HTTP_METHODS.POST, handler); 159 | } 160 | 161 | /** 162 | * @param {String} path 163 | * @param {RequestHandler} handler 164 | */ 165 | put(path, handler) { 166 | this.#addRoute(path, HTTP_METHODS.PUT, handler); 167 | } 168 | 169 | /** 170 | * @param {String} path 171 | * @param {RequestHandler} handler 172 | */ 173 | delete(path, handler) { 174 | this.#addRoute(path, HTTP_METHODS.DELETE, handler); 175 | } 176 | 177 | /** 178 | * @param {String} path 179 | * @param {RequestHandler} handler 180 | */ 181 | patch(path, handler) { 182 | this.#addRoute(path, HTTP_METHODS.PATCH, handler); 183 | } 184 | 185 | /** 186 | * @param {String} path 187 | * @param {RequestHandler} handler 188 | */ 189 | head(path, handler) { 190 | this.#addRoute(path, HTTP_METHODS.HEAD, handler); 191 | } 192 | 193 | /** 194 | * @param {String} path 195 | * @param {RequestHandler} handler 196 | */ 197 | options(path, handler) { 198 | this.#addRoute(path, HTTP_METHODS.OPTIONS, handler); 199 | } 200 | 201 | /** 202 | * @param {String} path 203 | * @param {RequestHandler} handler 204 | */ 205 | connect(path, handler) { 206 | this.#addRoute(path, HTTP_METHODS.CONNECT, handler); 207 | } 208 | 209 | /** 210 | * @param {String} path 211 | * @param {RequestHandler} handler 212 | */ 213 | trace(path, handler) { 214 | this.#addRoute(path, HTTP_METHODS.TRACE, handler); 215 | } 216 | 217 | /** 218 | * @param {RouteNode} node 219 | * @param {number} indentation 220 | */ 221 | printTree(node = this.root, indentation = 0) { 222 | const indent = "-".repeat(indentation); 223 | 224 | node.children.forEach((childNode, segment) => { 225 | console.log(`${indent}(${segment}) Dynamic: ${childNode.params}`); 226 | this.printTree(childNode, indentation + 1); 227 | }); 228 | } 229 | } 230 | 231 | module.exports = Router; 232 | -------------------------------------------------------------------------------- /src/chapter_06.12/challenge2/lib/utils.js: -------------------------------------------------------------------------------- 1 | const encodedMap = { 2 | "3A": ":", 3 | "2F": "/", 4 | "3F": "?", 5 | 23: "#", 6 | "5B": "[", 7 | "5D": "]", 8 | 40: "@", 9 | 21: "!", 10 | 24: "$", 11 | 26: "&", 12 | 27: "'", 13 | 28: "(", 14 | 29: ")", 15 | "2A": "*", 16 | "2B": "+", 17 | "2C": ",", 18 | "3B": ";", 19 | "3D": "=", 20 | 25: "%", 21 | 20: " ", 22 | 22: '"', 23 | "2D": "-", 24 | "2E": ".", 25 | 30: "0", 26 | 31: "1", 27 | 32: "2", 28 | 33: "3", 29 | 34: "4", 30 | 35: "5", 31 | 36: "6", 32 | 37: "7", 33 | 38: "8", 34 | 39: "9", 35 | 41: "A", 36 | 42: "B", 37 | 43: "C", 38 | 44: "D", 39 | 45: "E", 40 | 46: "F", 41 | 47: "G", 42 | 48: "H", 43 | 49: "I", 44 | "4A": "J", 45 | "4B": "K", 46 | "4C": "L", 47 | "4D": "M", 48 | "4E": "N", 49 | "4F": "O", 50 | 50: "P", 51 | 51: "Q", 52 | 52: "R", 53 | 53: "S", 54 | 54: "T", 55 | 55: "U", 56 | 56: "V", 57 | 57: "W", 58 | 58: "X", 59 | 59: "Y", 60 | "5A": "Z", 61 | 61: "a", 62 | 62: "b", 63 | 63: "c", 64 | 64: "d", 65 | 65: "e", 66 | 66: "f", 67 | 67: "g", 68 | 68: "h", 69 | 69: "i", 70 | "6A": "j", 71 | "6B": "k", 72 | "6C": "l", 73 | "6D": "m", 74 | "6E": "n", 75 | "6F": "o", 76 | 70: "p", 77 | 71: "q", 78 | 72: "r", 79 | 73: "s", 80 | 74: "t", 81 | 75: "u", 82 | 76: "v", 83 | 77: "w", 84 | 78: "x", 85 | 79: "y", 86 | "7A": "z", 87 | "5E": "^", 88 | "5F": "_", 89 | 60: "`", 90 | "7B": "{", 91 | "7C": "|", 92 | "7D": "}", 93 | "7E": "~", 94 | }; 95 | function fastDecode(string) { 96 | let result = ""; 97 | let lastIndex = 0; 98 | let index = string.indexOf("%"); 99 | 100 | while (index !== -1) { 101 | result += string.substring(lastIndex, index); 102 | const hexVal = string.substring(index + 1, index + 3); 103 | result += encodedMap[hexVal] || "%" + hexVal; 104 | lastIndex = index + 3; 105 | index = string.indexOf("%", lastIndex); 106 | } 107 | 108 | return result + string.substring(lastIndex); 109 | } 110 | 111 | module.exports = { fastDecode }; 112 | --------------------------------------------------------------------------------