diff --git a/cypress/e2e/docs-display.cy.ts b/cypress/e2e/docs-display.cy.ts index c3023c7b..1678fefa 100644 --- a/cypress/e2e/docs-display.cy.ts +++ b/cypress/e2e/docs-display.cy.ts @@ -13,6 +13,11 @@ const now = new Date(); const getDocsResponse: { result: PublicDocumentDto[] } = { result: [ { + score: { + average: 3.5, + count: 10, + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + }, id: `e9799f7b-013e-4231-88fe-e2072514f96a` as Atoms["DocumentId"], commentsCount: 0, name: `Mediator pattern in TypeScript`, diff --git a/cypress/e2e/docs-loading.cy.ts b/cypress/e2e/docs-loading.cy.ts index 439c704a..f3e5ed83 100644 --- a/cypress/e2e/docs-loading.cy.ts +++ b/cypress/e2e/docs-loading.cy.ts @@ -13,6 +13,11 @@ const now = new Date(); const getDocsResponse: { result: DocumentDto[] } = { result: [ { + score: { + average: 3.5, + count: 10, + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + }, id: `e9799f7b-013e-4231-88fe-e2072514f96a` as Atoms["DocumentId"], commentsCount: 1, name: `Mediator pattern in TypeScript`, @@ -41,6 +46,11 @@ const getDocsResponse: { result: DocumentDto[] } = { }, }, { + score: { + average: 3.5, + count: 10, + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + }, id: `8548ede2-5eb7-43b1-83ff-ecf0c75c6f18` as Atoms["DocumentId"], name: `Ideas for articles`, commentsCount: 1, @@ -51,6 +61,11 @@ const getDocsResponse: { result: DocumentDto[] } = { path: `/ideas-for-articles/` as Atoms["Path"], }, { + score: { + average: 3.5, + count: 10, + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + }, id: `8c6bf351-80a1-487a-b08b-ec8bbaaf2f6b` as Atoms["DocumentId"], name: `All about JavaScript promises`, code: "# All About **JavaScript Promises**\n\nProblems are the root of invention and progress. One of the most annoying issues that JavaScript developers faced several years ago was **Callback Hell**. The insane syntax and complexity of code frustrated many developers and made JavaScript appear unattractive to others.\n\nNow, the language is much more developer-friendly thanks to some abstractions that have been created. One of them is the `Promise`. Today, we'll learn everything about this API and master it. \n\n## Problems With **Callbacks**\n\nTake a look at Callback Hell - the main reason for `Promise` involvement:\n\n```javascript\n// Simulating asynchronous operations with callbacks.\nfunction asyncOperation1(callback) {\n setTimeout(() => {\n console.log(\"Operation 1 complete\");\n callback(null, \"Result of operation 1\");\n }, 1000);\n}\n\nfunction asyncOperation2(result1, callback) {\n setTimeout(() => {\n console.log(\"Operation 2 complete with input:\", result1);\n callback(null, \"Result of operation 2\");\n }, 1000);\n}\n\n// Callback hell example\nasyncOperation1((err, result1) => {\n if (err) {\n console.error(\"Error in operation 1:\", err);\n return;\n }\n asyncOperation2(result1, (err, result2) => {\n if (err) {\n console.error(\"Error in operation 2:\", err);\n return;\n }\n console.log(\"Final result:\", result2);\n });\n});\n```\n\nThe **JavaScript community** has created memes highlighting how insanely difficult it is to understand this syntax.\n\n![Callback Hell Meme](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2F7b96b4a6-a896-435b-8f9e-7b02383c20c7?alt=media)\n*Meme*\n\nIn addition, callbacks themselves are stateless and lack a unified structure. Different libraries may implement callbacks in various ways to pass an error, response, or other data, leading to inconsistencies.\n\n```javascript\n// Inconsistency...\nlib1((err, data) => {\n if (err) {\n return;\n }\n\n lib2(({ err, data }) => {\n if (err) {\n return;\n }\n\n // Do other stuff...\n });\n});\n```\n\nNext, you may notice the duplication of error handling logic. Every time, you need to add an if statement at every nested level of the code.\n\n```javascript\nlib1((err, data) => {\n // The repeated part.\n if (err) { \n return;\n }\n```\n\nIt's not the end yet :D. With the callback approach, we can clearly see that the behavior of each callback often depends on the previous one. As a result, the code we need to write to handle such behavior becomes very complex. Imagine needing to change the order of callbacks - good luck with that...\n\n```javascript\nasyncOperation1((err, result1) => {\n if (err) {\n console.error(\"Error in operation 1:\", err);\n return;\n }\n asyncOperation2(result1, (err, result2) => {\n if (err) {\n console.error(\"Error in operation 2:\", err);\n return;\n }\n console.log(\"Final result:\", result2);\n });\n});\n```\n\nLastly, there is a lack of easy control over code behavior. Imagine **React** without a built-in `useEffect` hook, where you would need to write convoluted code to listen for state changes or component updates - it would be a nightmare. To address this, **React** implemented a pattern called **Inversion of Control**. **React** provides an API to execute certain functions, and as developers, we only need to provide a function without worrying about when or how it will be called. **React** handles the invocation for us; we just specify the logic.\n\n```javascript\nconst ExampleComponent = () => {\n const [count, setCount] = useState(0);\n\n // useEffect runs after every render.\n useEffect(() => {\n // This is the effect logic.\n console.log(`Component rendered with count: ${count}`);\n\n // Optionally return a cleanup function.\n return () => {\n console.log(`Cleaning up after count: ${count}`);\n };\n }, [count]); // Dependency array, effect runs when `count` changes.\n}\n```\n\nWith callbacks, we need to specify the way and moment **imperatively**. That's why the `Promise` was added to the language - to solve these problems and make the syntax much easier to work with. Here is the same version of async code management, but now implemented with promises instead of callbacks.\n\n```javascript\nasyncOperation1()\n .then((result1) => {\n return asyncOperation2(result1);\n })\n .then((result2) => {\n return asyncOperation3(result2);\n })\n .then((result3) => {\n console.log(\"Final result:\", result3);\n })\n .catch((err) => {\n console.error(\"Error:\", err);\n });\n```\n\n## **Promises** In Theory\n\nHere is a documentation definition:\n\n> A `Promise` in **JavaScript** is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. It provides a cleaner and more manageable way to deal with asynchronous code compared to traditional **callback-based** approaches.\n\nThe `Promise` may have three states:\n\n1. **Pending**: the initial state, neither fulfilled nor rejected.\n2. **Fulfilled**: the operation completed successfully.\n3. **Rejected**: the operation failed.\n\nThe best way to understand promises is to compare them with a real-life situation. Imagine a letter that you're sending to a family member. The letter is hidden inside an envelope. Without the envelope, the letter carrier can see what you've sent, which is risky. Additionally, to provide information about where the letter should be delivered, you would need to destroy or change the form of the letter by including this information at the top or bottom. A much better way is to hide the letter inside an envelope.\n\nComparing this to promises, the delivery is an action that can have three possible states, just like a `Promise`(pending, fulfilled, or rejected). The **envelope** is a `Promise`, and the information attached to the envelope, like the destination address, is `Promise` metadata.\n\n> **Promises** are not only implemented in **JavaScript**. Although a **Promise** is not officially a design pattern, it is widely used to handle **asynchronous** operations. Promises are also implemented in languages such as `C#`, `Python`, `Java`, `Rust`, and `Swift`.\n\nIn summary, it's a way to describe an asynchronous operation that takes time to complete, attach metadata to it, and react to changes in the operation's state. Here is the diagram from [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise): \n\n![Promise Docs Diagram](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2Fcac52b06-13d3-4da2-b7db-9472e5b04447?alt=media)\n*Promise Diagram From Documentation*\n\n## Basic **Promise** Syntax\n\nLet's examine the theory. Imagine we're writing an app that allows users to send virtual letters. We have the following process:\n\n1. Writing a letter.\n2. Delivering the letter.\n3. Receiving a payment.\n\nAll of these are asynchronous operations may take time. It makes them perfect candidates for using a `Promise`. Additionally, we want to handle all errors that may occur during this process.\n\n```javascript\ninterface Letter {\n id: string;\n content: string;\n}\n\nconst writeLetter = (content: string): Promise => {\n return fetch(`/api/letter/create`, { method: 'post', body: content }).then(\n (res) => res.json()\n );\n};\n\nconst deliverLetter = (letter: Letter): Promise => {\n return fetch(`/api/letter/deliver`, {\n method: 'post',\n body: JSON.stringify(letter),\n }).then((res) => res.json());\n};\n\nconst receivePayment = (): Promise => {\n return fetch(`/api/payment/receive`).then((res) => res.json());\n};\n\nconst deliveryProcess = () => {\n writeLetter('My letter content')\n .then((letter) => deliverLetter(letter))\n .then(() => receivePayment())\n .catch((e: unknown) => {\n console.log(e);\n })\n .finally(() => {\n // It's \"fulfilled\" or \"rejected\" here.\n // Or we may call it \"settled\". \n });\n};\n\n// It runs the whole process.\ndeliveryProcess();\n```\n\nAs you saw, we've chained several asynchronous operations into one big process. Each `.then` continues the next step in the process - an API call that moves the process forward. After each step is fulfilled, the next `Promise` is created, and the next steps are involved. If an error occurs (rejected), we handle it once inside the `.catch` block.\n\nOn the other hand, whether the `Promise` is fulfilled or rejected, we can handle logic inside `.finally()` - such as clean-ups, logging, state resets, and many other aspects.\n\n> It's really important to understand that a `Promise` can be chained as many times as needed. It is simply a wrapper for an asynchronous operation. Thanks to the `then`, `catch`, and `finally` methods, it allows us to handle any process at a high level without unnecessary nesting.\n\n## The **Async** and **Await** Keywords\n\nThis makes promises even simpler. Instead of directly calling `then`, `catch`, or `finally`, we can use `async` and `await` to wrap code with built-in JavaScript exception handling.\n\n```javascript\ninterface Letter {\n id: string;\n content: string;\n}\n\nconst writeLetter = async (content: string): Promise => {\n return (\n await fetch(`/api/letter/create`, { method: 'post', body: content })\n ).json();\n};\n\nconst deliverLetter = async (letter: Letter): Promise => {\n return (\n await fetch(`/api/letter/deliver`, {\n method: 'post',\n body: JSON.stringify(letter),\n })\n ).json();\n};\n\nconst receivePayment = async (): Promise => {\n return (await fetch(`/api/payment/receive`)).json();\n};\n\nconst deliveryProcess = async () => {\n try {\n const letter = await writeLetter('My letter content');\n await deliverLetter(letter);\n await receivePayment();\n } catch (e: unknown) {\n console.log(e);\n } finally {\n // It's \"fulfilled\" or \"rejected\" here.\n }\n};\n\n// It runs the whole process.\ndeliveryProcess();\n```\n\nThe `async` keyword allows us to use `await`. Without the `async` keyword, we'll get a syntax error when trying to use `await`. Additionally, after declaring a function as `async`, we get the following behavior:\n\n1. If the function returns a value, the `Promise` is resolved with that value.\n2. If the function throws an error, the `Promise` is rejected with that error.\n\nEssentially, it automatically calls the `then` and `catch` methods for us, allowing us to write more linear code with less boilerplate - especially when transforming blocks like `.then(() => somePromiseFn())` to `await somePromiseFn()`.\n\nNow about `await`:\n\n1. When the `await` keyword is used, the `async` function is paused until the `Promise` is settled (fulfilled or rejected).\n2. If the `Promise` is fulfilled, the `await` expression returns the resolved value.\n3. If the `Promise` is rejected, the `await` expression throws the rejected value (similar to the `throw` statement).\n\nIn summary, it's just syntactic sugar, similar to how `class` is to the object prototype.\n\n## Converting **Async** Operations To **Promises**\n\nSometimes you're using legacy APIs, or you don't like the way they handle asynchronous operations. The `Promise` offers a way to handle such scenarios easily by converting async operations into `Promise` objects. Let's say you have a legacy library in the NodeJS for reading files:\n\n```javascript\nimport { read } from \"file-reader\";\n\nread(`/path-to-file`, (err, file) => {\n if (err) {\n console.log(err);\n return;\n }\n\n console.log(file);\n});\n```\n\nYou can easily convert it to a promise-based version with the following code:\n\n```javascript\nconst readPromise = (onSuccess) => {\n return new Promise((resolve, reject) => {\n read(`/path-to-file`, (err, file) => {\n if (err) {\n // The \"return\" ensures that \"resolve\" code is not called when an error occurs.\n return reject(err); \n }\n // In this case \"return\" is optional, as there is nothing more after this line.\n onSuccess();\n resolve(file);\n });\n });\n};\n```\n\nWe've used a `Promise` constructor that takes `resolve` and `reject` functions, allowing us to craft our custom `Promise` mechanism. Notice the important `return` statement in the error handling block. Without it, `onSuccess` could be called even when an error occurs, which is invalid behavior.\n\nIn the example code, we did not add `return` to `resolve`. In this case, it's fine, but if you have more complicated logic with multiple `if` statements, it's necessary to avoid bugs. To ensure consistency and avoid forgetting, I always add `return` to both `resolve` and `reject`.\n\n```javascript\nif (something) return reject(err);\nif (somethingElse) return resolve();\nreturn resolve();\n```\n\nThere is an ESLint plugin called [Consistent Return](https://eslint.org/docs/latest/rules/consistent-return) to help ensure that `return` statements are consistent.\n\n> Adding a `return` before `resolve` is generally a good practice, especially in functions with more complex logic. It ensures that the function exits immediately after calling `resolve` or `reject`, preventing any accidental execution of subsequent code. Choose this approach and be consistent.\n\n## **Promises** Chaining\n\nTo understand how it works, let's consider that we have several functions that return promises. Some of them will reject.\n\n```javascript\nconst randomPromiseFactory = (reject: boolean) => () =>\n reject ? Promise.reject(new Error('Error')) : Promise.resolve();\n\nconst p1 = randomPromiseFactory(false);\nconst p2 = randomPromiseFactory(true);\nconst p3 = randomPromiseFactory(false);\nconst p4 = randomPromiseFactory(false);\nconst p5 = randomPromiseFactory(true);\n```\n\nIn this code, the `p2` and `p5` functions will reject a value - the `reject` function will be called. All others will fulfill, meaning the `resolve` function will be called. Now, we want to achieve the following scenario:\n\n1. Call `p1`.\n2. Call `p2`.\n3. If any of the above fail, call `p3`.\n4. Call `p4` regardless of whether `p3` fails or succeeds.\n\nWe can use the following syntax:\n\n```javascript\n// Version 1\nconst chain = () => {\n p1()\n .then(() => p2())\n .catch(() => p3())\n .then(() => p4());\n};\n```\n\n```javascript\n// Version 2\nconst chain = async () => {\n try {\n await p1();\n await p2();\n } catch {\n await p3();\n }\n await p4();\n};\n```\n\nAs you saw, we've transformed each function invocation that returns a `Promise` into other promises to achieve the desired algorithm. What's really cool is that the nesting doesn't matter - you can have a deeply nested promise chain and catch errors at the top, or map the outcomes and handle them in the next `.then` blocks. Examine the following scenario:\n\n```javascript\nconst step1 = () => {\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve('Step 1 complete');\n }, 1000);\n });\n};\n\nconst step2 = () => {\n return new Promise((resolve, reject) => {\n setTimeout(() => {\n reject('Error in step 2');\n }, 1000);\n });\n};\n\nconst step3 = () => {\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve('Step 3 complete');\n }, 1000);\n });\n};\n\nstep1()\n .then(result => {\n console.log(result);\n return step2().then(step2Result => {\n console.log(step2Result);\n return step3();\n });\n })\n .then(result => {\n console.log(result);\n })\n .catch(error => {\n console.error('Caught error:', error);\n });\n\n// Output:\n// Step 1 complete\n// Caught error: Error in step 2\n```\n\nIn this example:\n\n- **Step 1** completes successfully and logs \"Step 1 complete\".\n- **Step 2** fails, and the error is caught by the `.catch()` block, logging \"Caught error: Error in step 2\".\n- **Step 3** is not called because the chain was interrupted by the rejection in **Step 2**.\n\n## **Static Methods** To Handle **Promises**\n\n### Promise.resolve(value)\n\nReturns a `Promise` that is resolved with the given value.\n\n```javascript\nPromise.resolve('Success').then(value => {\n console.log(value); // \"Success\"\n});\n```\n\n### Promise.reject(reason)\n\nReturns a `Promise` that is rejected with the given reason.\n\n```javascript\nPromise.reject('Error').catch(reason => {\n console.log(reason); // \"Error\"\n});\n```\n\n### Promise.all(iterable)\n\nReturns a `Promise` that resolves when all of the promises in the iterable have resolved. It rejects when any `Promise` in the iterable rejects.\n\n```javascript\nlet promise1 = Promise.resolve(1);\nlet promise2 = Promise.resolve(2);\n\nPromise.all([promise1, promise2]).then(values => {\n console.log(values); // [1, 2]\n});\n```\n\n### Promise.allSettled(iterable)\n\nReturns a `Promise` that resolves when all of the promises in the iterable have settled (either fulfilled or rejected). It never rejects. Instead, it passes an array to the fulfilled callback.\n\n```javascript\nlet promise1 = Promise.resolve(1);\nlet promise2 = Promise.reject('Error');\n\nPromise.allSettled([promise1, promise2]).then(results => {\n results.forEach(result => console.log(result.status));\n // \"fulfilled\"\n // \"rejected\"\n});\n```\n\n### Promise.any(iterable)\n\nReturns a `Promise` that resolves as soon as one of the promises in the iterable resolves, with the value from that promise. If no promises resolve or all rejects, it rejects with an `AggregateError`.\n\n```javascript\nlet promise1 = Promise.reject(\"Error\");\nlet promise2 = new Promise((resolve) => setTimeout(resolve, 100, \"two\"));\nlet promise3 = new Promise((resolve) => setTimeout(resolve, 200, \"one\"));\n\nPromise.any([promise1, promise2, promise3])\n .then((value) => {\n console.log(value); // It will prompt \"two\".\n })\n .catch((error) => {\n if (error instanceof AggregateError) {\n console.error(\n \"All promises were rejected. AggregateError:\",\n error.errors\n );\n } else {\n console.error(\"An unexpected error occurred:\", error);\n }\n });\n```\n\n**Use case**: You're developing an email sending service built on top of three different email providers. You want to send an email to a user following a system action. Using `Promise.any`, the `Promise` will resolve as soon as the fastest service sends the email. If all providers fail, you can handle the error using `AggregateError`.\n\n### Promise.race(iterable)\n\n`Promise.race` resolves or rejects as soon as one of the promises in the iterable it receives settles, regardless of whether the outcome is a resolution or rejection. Essentially, it returns the result of the fastest promise.\n\n```javascript\nlet promise1 = new Promise((resolve) => setTimeout(resolve, 500, 'one'));\nlet promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'two'));\nlet promise3 = Promise.reject(\"Error\");\n\nPromise.race([promise1, promise2]).then(value => {\n console.log(value); // Prompts \"two\".\n});\n\nPromise.race([promise1, promise2, promise3])\n .then((value) => {\n // It will be not called due to one rejection in \"promise3\".\n console.log(value);\n })\n .catch((error) => {\n // It will go to error \"catch\" block. \n console.error(\"An unexpected error occurred:\", error);\n });\n```\n\n> **Use Case**: You have several instances of a backend API managed by a custom load balancer, located in various parts of the world. You aim to route a request to the instance closest to the user to retrieve their data. Therefore, you want to return the result to the user immediately after receiving a response or rejection from the first instance that processes the request. \n\n### Promise.withResolvers()\n\nThis method is useful when you need to create and control a `Promise` instance that will be resolved or rejected later, often within a callback or asynchronous mechanism.\n\n```javascript\n// Example usage: Waiting for a button click.\nconst { promise, resolve, reject } = Promise.withResolvers();\n\nconst button = document.createElement('button');\nbutton.textContent = 'Click me';\ndocument.body.appendChild(button);\n\nbutton.addEventListener('click', () => {\n resolve('Button was clicked');\n button.remove(); // Clean up the button after it is clicked.\n});\n\n// Handling the promise.\npromise\n .then(message => {\n console.log(message);\n })\n .catch(error => {\n console.error(error);\n });\n\n// The promise will be resolved when the button is clicked.\n```\n\nThe key point here is that the `Promise` is not resolved immediately. It resolves only when the `resolve` function is called via a click event, which then triggers the `promise.then` method. This is possible because the `resolve` and `reject` functions are assigned to variables in a higher scope, allowing us to call them from anywhere without unnecessary nesting.\n\n## FAQs\n\n**If I call `resolve` or `reject` multiple times, will the `.then` or `.catch` handlers be called the same number of times?**\n\nNo, calling `resolve` or `reject` multiple times on a single promise will not cause the `.then` or `.catch` handlers to be called multiple times. Once a promise is settled (either fulfilled or rejected), its state is final and cannot be changed. Subsequent calls to `resolve` or `reject` will have no effect.\n\nHere’s an example to illustrate this behavior:\n\n```javascript\nconst promise = new Promise((resolve, reject) => {\n resolve('First value');\n resolve('Second value');\n reject('Error');\n});\n\npromise\n .then(value => {\n console.log('Resolved with:', value);\n })\n .catch(error => {\n console.error('Rejected with:', error);\n });\n\n// Logs: \"Resolved with: First value\"\n```\n\n**Should I always use `return` when using `Promise.reject` or `Promise.resolve`?**\n\nYes, it is generally a good practice to use `return` when calling `Promise.resolve` or `Promise.reject` inside a function, especially within complex logic or asynchronous callbacks. This ensures that the promise chain is properly maintained and can help avoid unexpected behaviors in future code changes.\n\nIf you have any logic under `reject` or `resolve`, it will still be executed. Remember, both `resolve` and `reject` may be called multiple times, but only the first call will affect the state of the promise. Subsequent calls will have no effect on the promise's state, but they may affect the logic within your function. Consider the following example:\n\n```javascript\nconst readPromise = () => {\n return new Promise((resolve, reject) => {\n read(`/path-to-file`, (err, file) => {\n if (err) {\n return reject(err); \n }\n // Ensure this function is not called if an error occurs.\n myOtherComplexFunction(); \n resolve(file);\n });\n });\n};\n```\n\nIn this example, using `return` with `reject` ensures that `myOtherComplexFunction()` is not called if an error occurs. Without the `return` statement, `myOtherComplexFunction()` would still be executed even if there was an error, which is undesirable behavior.\n\nSo, always adding a `return` statement for `reject` and `resolve` can help prevent such issues and keep your logic clear and predictable. This practice is already explained in the article to ensure you maintain proper promise behavior.\n\n**How do Promises work under the hood?**\n\nIn JavaScript, the execution of promises is managed by the **event loop** and the **microtask queue**. When a `Promise` is resolved or rejected, the corresponding `.then`, `.catch`, or `.finally` handlers are placed in the microtask queue.\n\nHere's the relevant API:\n\n```javascript\nqueueMicrotask(callback);\n```\n\nIt's important to understand that the callbacks passed to `.then`, `.catch`, and `.finally` are added to the **microtask queue**, not the internal logic of the **Promise** itself. The promise's internal logic is handled immediately in a synchronous manner.\n\n**What is the difference between `Promise.race` and `Promise.any`?**\n\nThe name `Promise.any` might seem misleading. While both functions aim to find the fastest `Promise`, they handle resolve and reject differently. Here are the differences:\n\n`Promise.race` takes multiple promises and returns the result of the first promise that settles, regardless of whether it resolves or rejects. It's like a race where the first promise to finish, for better or worse, ends the race.\n\n`Promise.any` takes multiple promises and returns the result of the first promise that resolves successfully. It ignores any promises that reject unless all of them fail. If all promises reject, it throws an `AggregateError`. Think of it as a contest where the first success wins, but all failures are ignored unless everyone fails.\n\n## Summary\n\nWow, that was a huge article, but I hope everything is now clear about the `Promise` concept. I found it useful to write this article because it refreshed my knowledge and clarified some gaps in my understanding. The most important concepts you should remember after reading this article are:\n\n1. A `Promise` is an object representing the eventual completion or failure of an **async operation**, with three possible states: **pending**, **fulfilled**, and **rejected**.\n2. The **fulfilled** or **rejected** states are collectively known as **settled**.\n3. A `Promise` is in the **pending** state immediately after the `Promise` instance is created.\n4. The **fulfilled** state occurs when the `resolve` function is called.\n5. The **rejected** state occurs when the `reject` function is called.\n6. Promises can be chained with `.then()` and `.catch()` methods, allowing for complex asynchronous workflows.\n7. The `async` and `await` provide syntactic sugar to work with Promises more easily.\n8. The `Promise` API has many static methods, such as `Promise.all`, `Promise.race`, `Promise.allSettled`, and `Promise.any`, to simplify logic.\n9. You can wrap current non-promise-based APIs with the `Promise` constructor to make them return promises.\n\nRemember to practice using this API to fully understand it. It can be particularly challenging on the backend with **Node.js**, and any gaps or misunderstandings can lead to many bugs later on.\n", @@ -79,6 +94,11 @@ const getDocsResponse: { result: DocumentDto[] } = { commentsCount: 1, }, { + score: { + average: 3, + values: [1, 2], + count: 2, + }, id: `1d8a4011-28a8-4720-b8df-9eb72819a181` as Atoms["DocumentId"], name: `Writing a parsing utility for Zod`, code: "# Writing a **Parsing Utility** For **Zod**\n\n**Zod** is a great library, there's no doubt about that. However, sometimes you may find yourself dealing with repetitive boilerplate code that the library may generate. The most common situation I've noticed is the preparation for **parsing** the schema and reading the validation result. This is especially prevalent on the backend side, where most validation occurs. Without a utility, you need to produce and repeat the following code:\n\n```javascript\n// It's just a part of a larger application code (but repetitive).\ntry {\n await schema.strict().parseAsync(payload);\n // Do some logic...\n} catch (err) {\n // Do some logic...\n logger.error(`Error occurred in ${name}`);\n logger.error(err);\n throw errors.invalidSchema(name);\n}\n```\n\nIf this code is duplicated in one or two files, it's not a problem. However, if duplicated in more than five places, it starts to become a warning sign. The typical parsing mechanism always looks similar in the context of any application. It involves:\n\n1. Schema to validate.\n2. Validating the schema.\n3. If an error occurs, parsing the error and throwing it.\n4. If no error, doing nothing or returning values.\n\nAdditionally, when performing validation, I try to be consistent. If the code is repeated across **n** files, there is a possibility that I may forget to use the `strict` or `parseAsync` function. You may not need them at all, but I mention them in the context of consistency, which might otherwise not be achieved.\n\nWith all this in mind, let's write a small utility for **Zod** parsing to remove some boilerplate and repetitiveness.\n\n## Implementation Of The **Parse** Function\n\nLet's design the contract of the function first. We want to have the following, easy-to-use signature.\n\n```javascript\nconst schema = z.object({\n id: z.string(),\n});\n// In this case, it should throw an error.\nconst result = parse(schema, { id: 1 });\n// In this case, it will return an object, correctly typed by \"Zod\".\nconst result1 = parse(schema, { id: \"1\" });\n```\n\nNow `parse` function implementation.\n\n```javascript\n// @@@ parse.ts\nimport { z, AnyZodObject } from 'zod';\nimport { errors } from './errors';\n\n// Validation of the passed generic schema \n// if it matches the Zod schema object.\nconst parse = async (\n schema: TSchema,\n payload: unknown,\n): Promise> => {\n try {\n // We're adding \"strict()\" and \"parseAsync()\" to every \n // call and staying consistent.\n const result = await schema.strict().parseAsync(payload);\n return result;\n } catch (e: unknown) {\n // If an error occurs, we're using a common \n // formatting utility. \n throw errors.schema(e);\n }\n};\n\nexport { parse };\n```\n\nFirst of all, we've created a generic type `TSchema` that must at least have the shape of `AnyZodObject`, imported from the **Zod** library. Then, we've passed a **payload**, which is really important here and is of type `unknown`. But why? When dealing with backend stuff, you don't have any guarantee that the passed object from the frontend is the type of object you expect. Frontend developers may pass anything, so it's naive to try typing it another way. The only valid option is to check it at runtime, and after doing so, we achieve **type safety**.\n\n> Type safety ensures that the types defined at compile time are strictly enforced at runtime, preventing type errors and ensuring consistent behavior. You can read more about this topic in the following article: [Why you should start using Zod](https://4markdown.com/why-you-should-start-using-zod/).\n\nSecondly, we wrapped all validation code in a repetitive `try, catch` block. Next, we've executed the validation, and we're returning the parsed values. If parsing fails, an exception will be thrown, and then we're parsing this exception object with the `errors.schema` utility function (I'll explain it in a second). The error is also of type `unknown` to force us, developers, to perform additional checks (type guards) before reading any information from such an object.\n\nThird, we're doing type inference with `z.infer` to produce a nice type for the consumer of this utility.\n\nLastly, here's a simple utility file `errors.ts` that contains the parsing logic for error objects:\n\n```javascript\nimport { https } from 'firebase-functions';\nimport { z } from 'zod';\n\nconst error = (\n code: https.FunctionsErrorCode,\n symbol: string,\n content: unknown,\n): https.HttpsError =>\n // It's an error object from Firebase, \n // but it can be anything else depending on the tech stack.\n new https.HttpsError(\n code,\n JSON.stringify({\n symbol,\n content,\n }),\n );\n\nconst errors = {\n internal: (content = `Something went wrong`) =>\n error(`internal`, `internal`, content),\n schema: (e: unknown) => {\n // Checking if the error is really a \"Zod\" error.\n if (e instanceof z.ZodError) {\n return error(\n `invalid-argument`,\n `invalid-schema`,\n // Mapping errors to the data supported by the frontend.\n e.errors.map(({ message, path }) => ({ message, key: path[0] })),\n );\n }\n\n return errors.internal();\n },\n};\n\nexport { errors };\n```\n\nThis file is responsible for typical error maintenance in the app. It may vary based on the tech stack you're using; in this example, we're returning **Firebase** error objects with content from **Zod** library. \n\n## Usage and Comparison\n\nNow, instead of having a lot of duplicated code, we can use a simple function to handle the **parsing** logic.\n\n```javascript\nconst before = async (payload: unknown) => {\n const schema = z.object({\n id: z.string(),\n });\n\n try {\n const result = await schema.strict().parseAsync(payload);\n // Do some logic...\n } catch (e: unknown) {\n throw errors.schema(e);\n }\n};\n```\n\n```javascript\nconst after = async (payload: unknown) => {\n const schema = z.object({\n id: z.string(),\n });\n\n const result = await parse(schema, payload);\n};\n```\n\nThe entire validation algorithm, parsing, and type inference are encapsulated in a single function. We've removed repetition and ensured the consistency of the validation mechanism. Now, all parsing involves a simple `strict` call and ensures that it uses `parseAsync` to boost performance slightly.\n\nThis approach really shines when you consider the amount of code you avoid writing and maintaining, especially when you have 10+ endpoints or similar use cases. As I mentioned at the beginning, it's not worth considering such **facades** for something that is not repeated multiple times and annoying to work with.\n\n## Summary\n\nToday we've created a useful utility function, `parse`, which is an implementation of the **facade pattern**. We've encapsulated some repetitive logic within a separate module. Instead of leaking this logic into every piece of application code, we now simply call the utility function and achieve the expected outcome.\n\nThe most important aspects to remember after reading this article are:\n\n1. If you have repetitive logic, wrap it into a **facade** and evaluate the benefits it provides.\n2. We've learned how to maintain and utilize built-in **Zod** generics.\n3. We've learned how to validate and parse errors using **Zod**.\n\n> Want to learn more about the **facade pattern**? Check out [The use case for facade pattern](https://4markdown.com/the-use-case-for-facade-pattern/) article.", diff --git a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Mediator Implementation.snap.png b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Mediator Implementation.snap.png index f45452c4..2e87d931 100644 Binary files a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Mediator Implementation.snap.png and b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Mediator Implementation.snap.png differ diff --git a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Notifications Management with Mediator.snap.png b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Notifications Management with Mediator.snap.png index 3452e74f..2c0ecb2e 100644 Binary files a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Notifications Management with Mediator.snap.png and b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Notifications Management with Mediator.snap.png differ diff --git a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Other Use Cases Ideas.snap.png b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Other Use Cases Ideas.snap.png index 2316e1be..e4f91d62 100644 Binary files a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Other Use Cases Ideas.snap.png and b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Other Use Cases Ideas.snap.png differ diff --git a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Summary.snap.png b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Summary.snap.png index d8393e93..0abe416d 100644 Binary files a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Summary.snap.png and b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Summary.snap.png differ diff --git a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Too Big Mediators - God Classes Issue.snap.png b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Too Big Mediators - God Classes Issue.snap.png index efdf4a48..6122e027 100644 Binary files a/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Too Big Mediators - God Classes Issue.snap.png and b/cypress/snapshots/docs-display.cy.ts/document-preview-heading-Too Big Mediators - God Classes Issue.snap.png differ diff --git a/gatsby-node.ts b/gatsby-node.ts index 0f778386..a4c188db 100644 --- a/gatsby-node.ts +++ b/gatsby-node.ts @@ -283,11 +283,12 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { const topDocuments = getTopDocuments(trustedDocuments, documentsPerPage).map< EducationPageModel["documents"]["top"][number] - >(({ author, name, id, path, rating, cdate, commentsCount }) => ({ + >(({ author, name, id, path, rating, cdate, commentsCount, score }) => ({ name, id, path, rating, + score, cdate, commentsCount, author: @@ -322,6 +323,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { name, id, path, + score, commentsCount, rating, cdate, @@ -332,6 +334,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { id, path, rating, + score, commentsCount, cdate, description, @@ -369,6 +372,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { ({ author, name, + score, id, path, rating, @@ -383,6 +387,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { path, rating, cdate, + score, description, tags, author: @@ -412,6 +417,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { id, path, rating, + score, cdate, description, tags, @@ -421,6 +427,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { id, path, rating, + score, cdate, description, commentsCount, diff --git a/src/api-4markdown-contracts/contracts.ts b/src/api-4markdown-contracts/contracts.ts index 2d1ed2e9..db560d88 100644 --- a/src/api-4markdown-contracts/contracts.ts +++ b/src/api-4markdown-contracts/contracts.ts @@ -238,6 +238,18 @@ type UserProfileCommentsContracts = type AccountsContracts = Contract<`getYourAccount`, YourAccountDto>; type DocumentsContracts = + | Contract< + `addDocumentScore`, + { + average: number; + count: number; + values: Atoms["ScoreValue"][]; + }, + { + documentId: Atoms["DocumentId"]; + score: Atoms["ScoreValue"]; + } + > | Contract< `getYourDocuments`, ( diff --git a/src/api-4markdown-contracts/dtos.ts b/src/api-4markdown-contracts/dtos.ts index 50c1a4c0..2f4fa5b5 100644 --- a/src/api-4markdown-contracts/dtos.ts +++ b/src/api-4markdown-contracts/dtos.ts @@ -163,7 +163,7 @@ export type FullMindmapDto = MindmapDto & { isAuthorTrusted: boolean; }; -type Base = { +type Base = Prettify<{ id: Atoms["DocumentId"]; name: string; commentsCount: number; @@ -172,31 +172,44 @@ type Base = { cdate: Atoms["UTCDate"]; sharedForGroups?: Atoms["AccessGroupId"][]; path: Atoms["Path"]; -}; + score: { + average: number; + count: number; + values: Atoms["ScoreValue"][]; + }; +}>; -export type PrivateDocumentDto = Base & { - visibility: "private"; -}; +export type PrivateDocumentDto = Prettify< + Base & { + visibility: "private"; + } +>; -export type PublicDocumentDto = Base & { - visibility: "public"; - author: UserProfileDto | null; - rating: Atoms["Rating"]; -}; +export type PublicDocumentDto = Prettify< + Base & { + visibility: "public"; + author: UserProfileDto | null; + rating: Atoms["Rating"]; + } +>; -export type PermanentDocumentDto = Base & { - visibility: `permanent`; - description: string; - tags: string[]; - author: UserProfileDto | null; - rating: Atoms["Rating"]; -}; +export type PermanentDocumentDto = Prettify< + Base & { + visibility: `permanent`; + description: string; + tags: string[]; + author: UserProfileDto | null; + rating: Atoms["Rating"]; + } +>; -export type ManualDocumentDto = Base & { - visibility: "manual"; - author: UserProfileDto | null; - rating: Atoms["Rating"]; -}; +export type ManualDocumentDto = Prettify< + Base & { + visibility: "manual"; + author: UserProfileDto | null; + rating: Atoms["Rating"]; + } +>; export type DocumentDto = | PrivateDocumentDto diff --git a/src/containers/document-layout.container.tsx b/src/containers/document-layout.container.tsx index 180d668a..4500c83c 100644 --- a/src/containers/document-layout.container.tsx +++ b/src/containers/document-layout.container.tsx @@ -10,8 +10,6 @@ import { BiLogoMarkdown, } from "react-icons/bi"; import { Button } from "design-system/button"; -import { useCopy } from "development-kit/use-copy"; -import { Status } from "design-system/status"; import { seeInDocumentsCreatorAct } from "acts/see-in-documents-creator.act"; import { Link, navigate } from "gatsby"; import { meta } from "../../meta"; @@ -28,9 +26,18 @@ import { useResourceCompletionToggle, useResourcesCompletionState, } from "modules/resource-completions"; -import { API4MarkdownPayload, Atoms } from "api-4markdown-contracts"; +import { + API4MarkdownDto, + API4MarkdownPayload, + Atoms, +} from "api-4markdown-contracts"; import { CommentTrigger } from "components/comment-trigger"; import { DocumentCommentsModule } from "modules/document-comments"; +import { ScorePicker } from "components/score-picker"; +import { useCopy } from "development-kit/use-copy"; +import { useMutation2 } from "core/use-mutation-2"; +import { getAPI } from "api-4markdown"; +import { toast } from "design-system/toast"; const MarkdownWidget = React.lazy(() => import("components/markdown-widget").then(({ MarkdownWidget }) => ({ @@ -39,6 +46,7 @@ const MarkdownWidget = React.lazy(() => ); const CONTENT_ID = `document-layout-content`; +const COMMENTS_CONTAINER_ID = `document-layout-comments`; const ResourceCompletionTriggerContainer = () => { const [{ document }] = useDocumentLayoutContext(); @@ -105,6 +113,34 @@ const DocumentLayoutContainer = () => { navigate(meta.routes.home); }; + const addScoreMutation = useMutation2>({ + onFail: (error) => { + toast.error({ + title: "Failed to add score", + children: error.message, + }); + }, + onOk: (data) => { + setDocumentLayoutState(({ document, yourRate }) => ({ + document: { + ...document, + score: { + average: data.average, + count: data.count, + values: data.values, + }, + }, + yourRate, + })); + }, + }); + + const addScore = (score: Atoms["ScoreValue"]): void => { + addScoreMutation.start(() => + getAPI().call("addDocumentScore")({ documentId: document.id, score }), + ); + }; + return ( <>
@@ -120,16 +156,7 @@ const DocumentLayoutContainer = () => { -
- - - { - window.scrollTo({ - top: Math.max( - window.document.documentElement.scrollHeight, - window.document.body.scrollHeight, - ), - }); - }} - /> + i={2} + onClick={sectionsModal.on} + > + + + +
+ + { + window.document + .getElementById(COMMENTS_CONTAINER_ID) + ?.scrollIntoView(); + }} + /> +
{document.visibility === `permanent` && ( @@ -206,18 +248,32 @@ const DocumentLayoutContainer = () => {
)} - - - setDocumentLayoutState(({ document, yourRate }) => ({ - yourRate, - document: { ...document, commentsCount: count }, - })) - } - commentsCount={document.commentsCount} - className="mt-10" - /> + +
+ +
+ + + +
+ + setDocumentLayoutState(({ document, yourRate }) => ({ + yourRate, + document: { ...document, commentsCount: count }, + })) + } + commentsCount={document.commentsCount} + className="mt-10" + /> +
@@ -229,7 +285,6 @@ const DocumentLayoutContainer = () => { )} - {copyState.is === `copied` && Document markdown copied} ); }; diff --git a/src/models/page-models.ts b/src/models/page-models.ts index 58f2292e..e3b587f8 100644 --- a/src/models/page-models.ts +++ b/src/models/page-models.ts @@ -23,13 +23,14 @@ type RichEducationDocumentModel = Pick< | "description" | "tags" | "commentsCount" + | "score" > & { author: EducationDocumentAvatarModel; }; type LightEducationDocumentModel = Pick< PermanentDocumentDto, - "id" | "path" | "name" | "rating" | "cdate" | "commentsCount" + "id" | "path" | "name" | "rating" | "cdate" | "commentsCount" | "score" > & { author: EducationDocumentAvatarModel; }; diff --git a/src/modules/document-comments/containers/document-comments.container.tsx b/src/modules/document-comments/containers/document-comments.container.tsx index 4f6adc1a..d0a49223 100644 --- a/src/modules/document-comments/containers/document-comments.container.tsx +++ b/src/modules/document-comments/containers/document-comments.container.tsx @@ -65,7 +65,7 @@ const DocumentCommentsContainer = ({ return ( <> -
+

Comments{" "} @@ -172,7 +172,7 @@ const DocumentCommentsContainer = ({ )} )} -

+ {commentForm.is === "on" && ( )}