├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── gatsby.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── csv.js ├── gatsby-config.js ├── gatsby-node.js ├── lessons ├── backoff-and-retry.md ├── chat-with-http2-push.md ├── chat-with-socketio.md ├── conclusion.md ├── images │ ├── FrontendMastersLogo.png │ ├── brian-beer.jpg │ ├── brian.jpg │ ├── logo.svg │ └── lunasit.jpg ├── intro-to-http2-push.md ├── intro-to-socketio.md ├── intro-to-websockets.md ├── intro.md ├── polling-backend.md ├── polling-with-settimeout.md ├── polling.md ├── requestanimationframe.md ├── the-project.md └── websockets-backend.md ├── package-lock.json ├── package.json ├── src ├── components │ └── TOCCard.js ├── layouts │ ├── Footer.css │ ├── Footer.js │ ├── corner-image.svg │ ├── github-social.svg │ ├── index.css │ ├── index.js │ ├── linkedin-social.svg │ ├── twitter-social.svg │ └── variables.css ├── pages │ ├── 404.js │ └── index.js ├── templates │ └── lessonTemplate.js └── util │ └── helpers.js └── static ├── author.jpg ├── corner-image-active.svg ├── corner-image-inactive.svg ├── courseImage.png └── posterframe.jpg /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "prettier", 8 | "prettier/react" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "jsx-a11y/label-has-for": 0, 13 | "no-console": 1 14 | }, 15 | "plugins": ["react", "import", "jsx-a11y"], 16 | "parser": "babel-eslint", 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module", 20 | "ecmaFeatures": { 21 | "jsx": true 22 | } 23 | }, 24 | "env": { 25 | "es6": true, 26 | "browser": true, 27 | "node": true 28 | }, 29 | "settings": { 30 | "react": { 31 | "version": "16.5.2" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/gatsby.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Gatsby Site to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: npm install, build, and csv 14 | run: | 15 | npm install 16 | npm run build 17 | npm run csv 18 | - name: Deploy site to gh-pages branch 19 | uses: crazy-max/ghaction-github-pages@v2 20 | with: 21 | target_branch: gh-pages 22 | build_dir: public 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 3 | node_modules 4 | .cache/ 5 | # Build directory 6 | public/ 7 | .DS_Store 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ## creative commons 2 | 3 | # Attribution-NonCommercial 4.0 International 4 | 5 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 6 | 7 | ### Using Creative Commons Public Licenses 8 | 9 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 10 | 11 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 12 | 13 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 14 | 15 | ## Creative Commons Attribution-NonCommercial 4.0 International Public License 16 | 17 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 18 | 19 | ### Section 1 – Definitions. 20 | 21 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 22 | 23 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 24 | 25 | c. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | 27 | d. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 28 | 29 | e. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 30 | 31 | f. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 32 | 33 | g. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 34 | 35 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 36 | 37 | i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 38 | 39 | j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 40 | 41 | k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 42 | 43 | l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 44 | 45 | ### Section 2 – Scope. 46 | 47 | a. ___License grant.___ 48 | 49 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 50 | 51 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 52 | 53 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 54 | 55 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 56 | 57 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 58 | 59 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 60 | 61 | 5. __Downstream recipients.__ 62 | 63 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 64 | 65 | B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 66 | 67 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 68 | 69 | b. ___Other rights.___ 70 | 71 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 72 | 73 | 2. Patent and trademark rights are not licensed under this Public License. 74 | 75 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 76 | 77 | ### Section 3 – License Conditions. 78 | 79 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 80 | 81 | a. ___Attribution.___ 82 | 83 | 1. If You Share the Licensed Material (including in modified form), You must: 84 | 85 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 86 | 87 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 88 | 89 | ii. a copyright notice; 90 | 91 | iii. a notice that refers to this Public License; 92 | 93 | iv. a notice that refers to the disclaimer of warranties; 94 | 95 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 96 | 97 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 98 | 99 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 100 | 101 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 102 | 103 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 104 | 105 | 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 106 | 107 | ### Section 4 – Sui Generis Database Rights. 108 | 109 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 110 | 111 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 112 | 113 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 114 | 115 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 116 | 117 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 118 | 119 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 120 | 121 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 122 | 123 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 124 | 125 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 126 | 127 | ### Section 6 – Term and Termination. 128 | 129 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 130 | 131 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 132 | 133 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 134 | 135 | 2. upon express reinstatement by the Licensor. 136 | 137 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 138 | 139 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 140 | 141 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 142 | 143 | ### Section 7 – Other Terms and Conditions. 144 | 145 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 146 | 147 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 148 | 149 | ### Section 8 – Interpretation. 150 | 151 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 152 | 153 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 154 | 155 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 156 | 157 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 158 | 159 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 160 | > 161 | > Creative Commons may be contacted at creativecommons.org -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
9 | The Complete Intro to Realtime, as taught by Brian Holt for Frontend Masters 10 |
11 | 12 | [][fem] 13 | 14 | [Please click here][course] to head to the course website. 15 | 16 | # Issues and Pull Requests 17 | 18 | Please file issues and open pull requests here! Thank you! For issues with project files, either file issues on _this_ repo _or_ open a pull request on the projects repos. This repo itself is the course website. 19 | 20 | # Project Files 21 | 22 | [Please go here][project] for the project files. 23 | 24 | # License 25 | 26 | The content of this workshop is licensed under CC-BY-NC-4.0. Feel free to share freely but do not resell my content. 27 | 28 | The code, including the code of the site itself and the code in the exercises, are licensed under Apache 2.0. 29 | 30 | # Attributions 31 | 32 | Icons made by Freepik from www.flaticon.com 33 | 34 | [fem]: https://frontendmasters.com/ 35 | [course]: https://btholt.github.io/complete-intro-to-realtime 36 | [project]: https://github.com/btholt/realtime-projects/ 37 | -------------------------------------------------------------------------------- /csv.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs").promises; 2 | const path = require("path"); 3 | const fm = require("front-matter"); 4 | const isUrl = require("is-url-superb"); 5 | const parseLinks = require("parse-markdown-links"); 6 | const { sorter } = require("./src/util/helpers"); 7 | const mdDir = process.env.MARKDOWN_DIR || path.join(__dirname, "lessons/"); 8 | const outputPath = 9 | process.env.OUTPUT_CSV_PATH || path.join(__dirname, "public/lessons.csv"); 10 | const linksOutputPath = 11 | process.env.LINKS_CSV_PATH || path.join(__dirname, "public/links.csv"); 12 | 13 | async function createCsv() { 14 | console.log(`making the markdown files into a CSV from ${mdDir}`); 15 | 16 | // get paths 17 | const allFiles = await fs.readdir(mdDir); 18 | const files = allFiles.filter(filePath => filePath.endsWith(".md")); 19 | 20 | // read paths, get buffers 21 | const buffers = await Promise.all( 22 | files.map(filePath => fs.readFile(path.join(mdDir, filePath))) 23 | ); 24 | 25 | // make buffers strings 26 | const contents = buffers.map(content => content.toString()); 27 | 28 | // make strings objects 29 | let frontmatters = contents.map(fm); 30 | 31 | // find all attribute keys 32 | const seenAttributes = new Set(); 33 | frontmatters.forEach(item => { 34 | Object.keys(item.attributes).forEach(attr => seenAttributes.add(attr)); 35 | }); 36 | const attributes = Array.from(seenAttributes.values()); 37 | 38 | if (attributes.includes("order")) { 39 | frontmatters = frontmatters.sort(sorter); 40 | } 41 | 42 | // get all data into an array 43 | let rows = frontmatters.map(item => { 44 | const row = attributes.map(attr => 45 | item.attributes[attr] ? JSON.stringify(item.attributes[attr]) : "" 46 | ); 47 | return row; 48 | }); 49 | 50 | // header row must be first row 51 | rows.unshift(attributes); 52 | 53 | // join into CSV string 54 | const csv = rows.map(row => row.join(",")).join("\n"); 55 | 56 | // write file out 57 | await fs.writeFile(outputPath, csv); 58 | 59 | console.log(`Wrote ${rows.length} rows to ${outputPath}`); 60 | 61 | // make links csv 62 | let longestLength = 0; 63 | let linksArray = frontmatters.map(row => { 64 | const links = parseLinks(row.body).filter(isUrl); 65 | longestLength = longestLength > links.length ? longestLength : links.length; 66 | const newRow = [row.attributes.order, row.attributes.title, ...links]; 67 | return newRow; 68 | }); 69 | 70 | if (longestLength) { 71 | // add title row 72 | linksArray = linksArray.map(array => { 73 | const lengthToFill = longestLength + 2 - array.length; 74 | return array.concat(Array.from({ length: lengthToFill }).fill("")); 75 | }); 76 | 77 | linksArray.unshift( 78 | ["order", "title"].concat( 79 | Array.from({ length: longestLength }).map((_, index) => `link${index}`) 80 | ) 81 | ); 82 | 83 | // join into CSV string 84 | const linksCsv = linksArray.map(row => row.join(",")).join("\n"); 85 | 86 | // write file out 87 | await fs.writeFile(linksOutputPath, linksCsv); 88 | 89 | console.log(`Wrote ${linksArray.length} rows to ${linksOutputPath}`); 90 | } 91 | } 92 | 93 | createCsv(); 94 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: "Complete Intro to Realtime", 4 | subtitle: "Websockets, Polling, and More", 5 | author: "Brian Holt", 6 | authorSubtitle: "Stripe", 7 | authorImage: "author.jpg", // this image should go in /static 8 | courseImage: "courseImage.png", // this also should go in /static 9 | twitter: "https://twitter.com/holtbt", // make empty string to omit socials 10 | linkedin: "https://linkedin.com/in/btholt", 11 | github: "https://github.com/btholt", 12 | description: 13 | "Learn how to do realtime communcation on the web with your teacher Brian Holt", 14 | keywords: [ 15 | "realtime", 16 | "websockets", 17 | "polling", 18 | "signalr", 19 | "node.js", 20 | "javascript", 21 | "frontend", 22 | ], 23 | }, 24 | pathPrefix: "/complete-intro-to-realtime", // if you're using GitHub Pages, put the name of the repo here with a leading slash 25 | plugins: [ 26 | { 27 | resolve: "gatsby-plugin-react-svg", 28 | options: { 29 | rule: { 30 | include: /src/, 31 | }, 32 | }, 33 | }, 34 | `gatsby-plugin-sharp`, 35 | `gatsby-plugin-layout`, 36 | { 37 | resolve: `gatsby-source-filesystem`, 38 | options: { 39 | path: `${__dirname}/lessons`, 40 | name: "markdown-pages", 41 | }, 42 | }, 43 | `gatsby-plugin-react-helmet`, 44 | { 45 | resolve: `gatsby-transformer-remark`, 46 | options: { 47 | plugins: [ 48 | `gatsby-remark-autolink-headers`, 49 | `gatsby-remark-copy-linked-files`, 50 | `gatsby-remark-prismjs`, 51 | { 52 | resolve: `gatsby-remark-images`, 53 | options: { 54 | maxWidth: 800, 55 | linkImagesToOriginal: true, 56 | sizeByPixelDensity: false, 57 | }, 58 | }, 59 | ], 60 | }, 61 | }, 62 | ], 63 | }; 64 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | exports.createPages = ({ actions, graphql }) => { 4 | const { createPage } = actions; 5 | 6 | const lessonTemplate = path.resolve(`src/templates/lessonTemplate.js`); 7 | 8 | return graphql(` 9 | { 10 | allMarkdownRemark( 11 | sort: { order: DESC, fields: [frontmatter___order] } 12 | limit: 1000 13 | ) { 14 | edges { 15 | node { 16 | excerpt(pruneLength: 250) 17 | html 18 | id 19 | frontmatter { 20 | order 21 | path 22 | title 23 | } 24 | } 25 | } 26 | } 27 | } 28 | `).then(result => { 29 | if (result.errors) { 30 | return Promise.reject(result.errors); 31 | } 32 | 33 | result.data.allMarkdownRemark.edges.forEach(({ node }) => { 34 | createPage({ 35 | path: node.frontmatter.path, 36 | component: lessonTemplate 37 | }); 38 | }); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /lessons/backoff-and-retry.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/backoff-and-retry" 3 | title: "Backoff and Retry" 4 | order: "2E" 5 | section: "Polling" 6 | description: "" 7 | --- 8 | 9 | What is a polling request fails? You don't want to thundering-herd yourself by hammering your own API with more requests, but you also want the user to get back in once it's not failing anymore. We'll look at strategies to mitigate that. 10 | 11 | The worst case scenario is what we called the thundering herd. I've discussed this previously in my databases courses when talking about caches and how you can overwhelm your servers when your cache misses and every user directly hits your server. This is a similar problem we can cause with polling where we have an error in our polling so every user immediately makes another request to try to recover. When that request fails again, the poller will try again. Request after request. Now imagine this at 10,000x the scale. You basically wrote code to [DDoS][ddos] yourself. 12 | 13 | So how do we balance a good user experience (recovering as fast as possible) with technical needs (allowing your servers space to recover)? Back off strategies! 14 | 15 | Let's imagine for a second our chat app polls every thirty seconds. That's enough time for them to notice that we skipped a poll; they'd be waiting a minute between polls. And since many errors are not servers crashing but intermittent blips of a load balancer or latency or really anything else besides your whole service going down, it'd be best to not wait the whole next thirty seconds but to try again immediately. Assuming the server isn't down, you'd prevent the user from ever knowing something was amiss. 16 | 17 | So on an API failure, we should immediately try again to see if we can recover. Assuming it goes well and we get a 200 OK response, then all is well and we continue polling as normal. Okay, but what if it fails again? Well, here you can try several strategies of how to do the math but the idea is you wait increasing intervals. First try again after 10 seconds, then 20, then 30, then 40, etc. Or you could exponential backoff and wait 2, 4, 8, 16, 32, 64, 128, etc. You'll have to choose a backoff strategy that works best for your usecase depending on vital is it to recover immediately and how difficult it will be for your servers to recover. 18 | 19 | Let's do a pretty simple strategy of exponential back off by doing 3 seconds (our default poll length) _plus_ 5 seconds times how many times we've tried. So if we've had four errors in a row, it'd take 23 seconds before we'd try again (3 + 4 \* 5). So let's go do it together in polling-chat.js 20 | 21 | ```javascript 22 | // replace getNewMsgs 23 | async function getNewMsgs() { 24 | try { 25 | const res = await fetch("/poll"); 26 | const json = await res.json(); 27 | 28 | if (res.status >= 400) { 29 | throw new Error("request did not succeed: " + res.status); 30 | } 31 | 32 | allChat = json.msg; 33 | render(); 34 | failedTries = 0; 35 | } catch (e) { 36 | // back off 37 | failedTries++; 38 | } 39 | } 40 | 41 | // replace at bottom 42 | const BACKOFF = 5000; 43 | let timeToMakeNextRequest = 0; 44 | let failedTries = 0; 45 | async function rafTimer(time) { 46 | if (timeToMakeNextRequest <= time) { 47 | await getNewMsgs(); 48 | timeToMakeNextRequest = time + INTERVAL + failedTries * BACKOFF; 49 | } 50 | requestAnimationFrame(rafTimer); 51 | } 52 | 53 | requestAnimationFrame(rafTimer); 54 | ``` 55 | 56 | Awesome! Now this backoff an additional 5 seconds every time a request fails. 57 | 58 | If you want to try it, replace you get in server.js with this 59 | 60 | ```javascript 61 | app.get("/poll", function (req, res) { 62 | res.status(Math.random() > 0.5 ? 200 : 500).json({ 63 | msg: getMsgs(), 64 | }); 65 | }); 66 | ``` 67 | 68 | This will 500 (which means server error) 50% of the requests to poll to show you it recovering and backing off. 69 | 70 | So a few things here: 71 | 72 | - fetch does throw an error if there's a network error like not being able to connect to the Internet, hence the try/catch 73 | - fetch does not throw an error if the server responds with a 500, hence the throw statement to make sure those errors get handled the same way 74 | - This is just one way of doing the math. Feel free to look at other strategies and decide what's best for you. 75 | 76 | > Here's my default strategy when I do it: on a failed request, wait a few seconds, try again. This will catch most incidental blips like your user changed networks or something like that. If fails, wait a few more seconds and try again. This will catch _most_ people who are having temporary issues. After two failed requests, we can assume we're having either server problems (or the user is offline totally or something not temporary on their side.) This is when I want to start putting bigger gaps in time between requests so I don't overwhelm my servers while I'm trying to recover. 77 | 78 | In general I don't write retry logic myself. There are numerous, numerous packages on npm that handle backoff and retry with lots of unit tests and users so I tend to just rely on that. If I did, I'd write one central library for my project to rely on and then I'd test the hell out of it. But it's good for you to know how to write this! 79 | 80 | > The current state of the repo can be found in the [backoff-and-retry][gh] directory on the project. 81 | 82 | [gh]: https://github.com/btholt/realtime-exercises/tree/main/polling/backoff-and-try 83 | [ddos]: https://en.wikipedia.org/wiki/Denial-of-service_attack 84 | -------------------------------------------------------------------------------- /lessons/chat-with-http2-push.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/chat-with-http2-push" 3 | title: "Chat with HTTP2 Push" 4 | order: "3B" 5 | description: "" 6 | section: "HTTP2" 7 | --- 8 | 9 | > Open your project to [http2/exercise][exercise] to start this project. 10 | 11 | Alright, so let's start digging into it. 12 | 13 | First, a housekeeping thing, HTTP/2 does not work without HTTPS because all the browsers enforce that it must be a secure connection. Technically the spec doesn't require it but our stuff won't work otherwise so we need to quickly generate a self-signed certificate to use for our app. 14 | 15 | You'll need to install openssl. On macOS, you can install it via [Homebrew][homebrew] with `brew install openssl`. If you're on Linux, it'll probably be available by whatever your distro's package manager is. If you're on Windows, you can either Google ["install openssl windows"][google] or use [Chocolatey][c]. 16 | 17 | Once you have it installed, go to the _root_ directory of your exercise. For you, that's probably http2/exercise, and run these two commands. 18 | 19 | > It's important that these are located not in the backend directory but the root directory of your http2 exercise project. 20 | 21 | ```bash 22 | openssl req -new -newkey rsa:2048 -new -nodes -keyout key.pem -out csr.pem 23 | openssl x509 -req -days 365 -in csr.pem -signkey key.pem -out server.crt 24 | ``` 25 | 26 | > It'll ask you a bunch of questions, it doesn't matter how you answer them. 27 | 28 | This will generate a self-signed certificate that we can use for local dev. This wouldn't work in production because it'll show up as insecure to your users. When you bring up your own site, it'll warn you that the cert is self-signed. This is normal. 29 | 30 | From here you should be able to run `npm run dev` and it should work. Make sure you're on httpS://localhost:8080, with the S. It won't work on http. You need https. 31 | 32 | Okay, so let's open our backend/server.js and work on that. Notice at the top we're loading those two things we generated. We'll come back to the code block we have to write, but notice on the `server.on("request")`. Here we're first statically serving all our static assets. After that we're accepting the POST data. This is something Express does for us but since we're using raw HTTP2 from Node.js we have to accept that data ourselves. No big deal. 33 | 34 | Okay, let's work with our stream of data. 35 | 36 | ```javascript 37 | // above server.on('request') 38 | server.on("stream", (stream, headers) => { 39 | const method = headers[":method"]; 40 | const path = headers[":path"]; 41 | 42 | // streams will open for everything, we want just GETs on /msgs 43 | if (path === "/msgs" && method === "GET") { 44 | // immediately respond with 200 OK and encoding 45 | console.log("connected"); 46 | stream.respond({ 47 | ":status": 200, 48 | "content-type": "text/plain; charset=utf-8", 49 | }); 50 | 51 | // write the first response 52 | stream.write(JSON.stringify({ msg: getMsgs() })); 53 | 54 | stream.on("close", () => { 55 | console.log("disconnected"); 56 | }); 57 | } 58 | }); 59 | ``` 60 | 61 | So here we're responding to stream requests. All resources actually open stream requests, so we're just watching for connections to /msgs (Express would normally handle this routing.) Once we have a stream request, we'll immediately respond with headers to say how it's encoded, what type of protocol we're using, and a 200 OK. We're using text/plain because while we'll be sending JSON, if you wholistically looked at our response, it wouldn't actually be valid JSON. But at the end of the day it doesn't matter. 62 | 63 | Okay let's pop over to the client. Open the http2-chat.js file. 64 | 65 | ```javascript 66 | // replace getNewMsgs 67 | async function getNewMsgs() { 68 | let reader; 69 | const utf8Decoder = new TextDecoder("utf-8"); 70 | try { 71 | const res = await fetch("/msgs"); 72 | reader = res.body.getReader(); 73 | } catch (e) { 74 | console.log("connection error", e); 75 | } 76 | presence.innerText = "🟢"; 77 | 78 | try { 79 | readerResponse = await reader.read(); 80 | const chunk = utf8Decoder.decode(readerResponse.value, { stream: true }); 81 | console.log(chunk); 82 | } catch (e) { 83 | console.error("reader failed", e); 84 | presence.innerText = "🔴"; 85 | return; 86 | } 87 | } 88 | ``` 89 | 90 | A few things here: 91 | 92 | - We're still using fetch, but instead of just saying res.json(), we're opening a readable stream with getReader() and now we can expect multiple responses. Up front we're just logging out the first chunk we get back, but we can now expect that to respond multiple times. 93 | - We're using the green and red circle to show the user if they're still connected to the socket. If it's red, we know we've disconnected. If that happens, you just need to refresh the page. In a production app, you'd just need to reconnect a new socket and keep listening. But you can do that on your own time. 94 | - We need to decode the response that comes over the socket. That's what the utf8Decoder does. 95 | - I'm not making you do the POST again. It's the same logic as last time. 96 | - If you look at the network request in your network console, notice that there isn't a status code or anything. According to the browser, this request is still actually in flight. 97 | 98 | This will only read the very first chunk from the API. We can use a do/while loop with await to make it work as long as the socket is still sending info. 99 | 100 | ```javascript 101 | // inside getNewMsgs, replace the second, bottom try/catch 102 | do { 103 | let readerResponse; 104 | try { 105 | readerResponse = await reader.read(); 106 | } catch (e) { 107 | console.error("reader failed", e); 108 | presence.innerText = "🔴"; 109 | return; 110 | } 111 | done = readerResponse.done; 112 | const chunk = utf8Decoder.decode(readerResponse.value, { stream: true }); 113 | if (chunk) { 114 | try { 115 | const json = JSON.parse(chunk); 116 | allChat = json.msg; 117 | render(); 118 | } catch (e) { 119 | console.error("parse error", e); 120 | } 121 | } 122 | } while (!done); 123 | // in theory, if our http2 connection closed, `done` would come back 124 | // as true and we'd no longer be connected 125 | presence.innerText = "🔴"; 126 | ``` 127 | 128 | This will go forever, as long as that socket is open. That's what the do/while loop will do for you. Since we never actually close this connection, it'll stay open as long as there isn't a browser event (like a user closing the tab or the user closing the laptop for long enough to disconnect all active connections.) Here we're just treating each chunk as an API response, but in reality it's supposed to be a continuous stream of data of one document, hence why I called this a bit of abuse of the system. 129 | 130 | Let's go finish up server.js 131 | 132 | ```javascript 133 | // inside of server.on("streams") 134 | // under stream.write 135 | // replace stream.on("close") 136 | 137 | // keep track of the connection 138 | connections.push(stream); 139 | 140 | // when the connection closes, stop keeping track of it 141 | stream.on("close", () => { 142 | connections = connections.filter((s) => s !== stream); 143 | }); 144 | 145 | // inside server.on("request") 146 | // under const { user, text } = JSON.parse(data); 147 | msg.push({ 148 | user, 149 | text, 150 | time: Date.now(), 151 | }); 152 | 153 | // all done with the request 154 | res.end(); 155 | 156 | // notify all connected users 157 | connections.forEach((stream) => { 158 | stream.write(JSON.stringify({ msg: getMsgs() })); 159 | }); 160 | ``` 161 | 162 | This is now a fully functioning realtime app with HTTP/2 push! We're now keeping track of all open streams and when we get a new post from anyone, we're writing to our open streams. Once a user closes out their stream we remove from our list to notify. 163 | 164 | > The finished version of the app can be found here: [http2/push][gh] 165 | 166 | That's it! We did it! Let's move onto WebSockets! 167 | 168 | [c]: https://community.chocolatey.org/packages?q=openssl 169 | [google]: https://www.google.com/search?q=install+openssl+windows 170 | [homebrew]: https://brew.sh/ 171 | [exercise]: https://github.com/btholt/realtime-exercises/tree/main/http2/exercise 172 | [gh]: https://github.com/btholt/realtime-exercises/tree/main/http2/push 173 | -------------------------------------------------------------------------------- /lessons/chat-with-socketio.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/chat-with-socketio" 3 | title: "Chat with Socket.IO" 4 | order: "5B" 5 | description: "" 6 | section: "Socket.IO" 7 | --- 8 | 9 | > Open the project directory to [websockets/exercise-socketio][start] 10 | 11 | This is going to be 100x teams easier than what we were doing before because we don't have to handle the raw mechanics of accepting a connection, negotiating an upgrade, handling the handshake to acknowledge what sort WebSocket connection it will be and what protocol they'll speak with, and so on. Instead, we just get to accept a connection. 12 | 13 | In exercise-socketio/backend/server.js, let's do the following 14 | 15 | ```javascript 16 | const io = new Server(server, {}); 17 | 18 | io.on("connection", (socket) => { 19 | console.log(`connected: ${socket.id}`); 20 | 21 | socket.on("disconnect", () => { 22 | console.log(`disconnect: ${socket.id}`); 23 | }); 24 | }); 25 | ``` 26 | 27 | That's it, at least from a server perspective, to a hello-world Socket.IO app. I want you to take a take a second and pause and appreciate how little we had to do get that working. Whereas before we had to worry about the upgrade, the magic key, the sha1 hash, the base64 response, the data frames, and the binary format, this is _all_ we have to do here. The rest is just handled for us. Can you believe that? It barely seems possible. It's a good moment to reflect on how grateful we should be open source and that a lot of this complexity just gets handled by really smart and hard-working people and we get to stand on the shoulders of these giants. 28 | 29 | Let's implement the frontend connection. In exercise-socketio/frontend/socketio-chat.js 30 | 31 | ```javascript 32 | const socket = io("http://localhost:8080"); 33 | 34 | socket.on("connect", () => { 35 | console.log("connected"); 36 | presence.innerText = "🟢"; 37 | }); 38 | 39 | socket.on("disconnect", () => { 40 | presence.innerText = "🔴"; 41 | }); 42 | ``` 43 | 44 | The `io` library is being loaded from CDN for you. Normally you'd npm install it and import but for the sake of not having you mess with build tools for the one library I just loaded it for you. 45 | 46 | So this code looks really similar to what we had before which is to be expected: from the client's perspective, it doesn't really change too much. But let's talk about two key differences. 47 | 48 | 1. Like we mentioned before, if your browser couldn't handle sockets (as of writing 98% of browsers can, since IE10) it will fall back to polling and your code doesn't need to change. 49 | 1. There is automatic retry logic if your connection closes unexpectedly. Remember in our previous version if the server restarted because we saved it would close the connection. Try that here. Notice our presence indicator will go red for a sec and then back to green. This is free and built into Socket.IO. 50 | 51 | Let's implement the initial get. In server.js 52 | 53 | ```javascript 54 | // inside io.on("connection") under console.log 55 | socket.emit("msg:get", { msg: getMsgs() }); 56 | ``` 57 | 58 | Okay, well, that's pretty straightforward. Let's read off the socket in socketio-chat.js 59 | 60 | ```javascript 61 | socket.on("msg:get", (data) => { 62 | allChat = data.msg; 63 | render(); 64 | }); 65 | ``` 66 | 67 | Let's talk about "msg:get". With Socket.IO that string represents an event name, similar to a browser. Here we're saying we're receiving a "msg:get" library from the server (which we emitted above) and that's when we should expect data. If you've ever done Pub/Sub, that's exactly what this is. 68 | 69 | The colon in the middle is what we'd call namespacing. The first bit, the msg part, is the namespace. All "msg" related events will be inside of it. The "get", the last part, represents what actions we're looking to do in that namespace. In reality, it's just a string, this is just a common pattern you'll in Pub/Sub in general, not just Socket.IO. 70 | 71 | Okay, so let's finish out now. In socketio-chat.js 72 | 73 | ```javascript 74 | // replace postNewMsg 75 | async function postNewMsg(user, text) { 76 | const data = { 77 | user, 78 | text, 79 | }; 80 | 81 | socket.emit("msg:post", data); 82 | } 83 | ``` 84 | 85 | We're using the same emit function, just from the client. It's nice to learn the semantics once and apply it multiple ways. Now in server.js 86 | 87 | ```javascript 88 | // beneath socket.emit("msg:get") 89 | socket.on("msg:post", (data) => { 90 | msg.push({ 91 | user: data.user, 92 | text: data.text, 93 | time: Date.now(), 94 | }); 95 | io.emit("msg:get", { msg: getMsgs() }); 96 | }); 97 | ``` 98 | 99 | First part looks familiar, add to our existing data structure. However a big key different lies in the io.emit part. Notice we're using io instead of socket to call emit. Why? Well, we don't want to _just_ emit the msg:get back to the client who posted to us, we want to emit to _all_ clients listening to us. The easy way to do this is just to call io.emit. Try it! Open a second window and try chatting between two clients. Much easier than trying to keep track of all the clients ourselves. 100 | 101 | And that's it! Again, we scratched the surface here but I wanted to show you how easy it is to get off to the races with realtime and particularly WebSockets wit Socket.IO. Cool ways you could expand this project. 102 | 103 | - Convert from a JavaScript project to a React or other framework project to see how to add realtime to your frameworks 104 | - Make it so users can private message each other based on their usernames 105 | - Add authentication and authorization 106 | - Add rooms so people can jump in and out of various rooms 107 | - Add a super user that can ban usernames 108 | - Connect your server to a data store (like Redis) so chat histories can survive restarts 109 | 110 | Congrats! 111 | 112 | > The finished code can be found in the project at [websockets/socketio][gh] 113 | 114 | [gh]: https://github.com/btholt/realtime-exercises/tree/main/websockets/socketio 115 | [start]: https://github.com/btholt/realtime-exercises/tree/main/websockets/exercise-socketio 116 | -------------------------------------------------------------------------------- /lessons/conclusion.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/conclusion" 3 | title: "Conclusion" 4 | order: "6A" 5 | description: "" 6 | section: "The End" 7 | icon: "check-circle" 8 | --- 9 | 10 | You did it! You learned realtime and several techniques you can use to go about doing realtime communication. Before we wrap up, I want to talk about a few additional pieces of connections you can do that we didn't talk about. 11 | 12 | ## ws 13 | 14 | [ws][ws] is the other leading implementation of WebSockets for Node.js and a damn good one. Frankly most of the time I choose it over Socket.IO because it's more minimal and I don't need the richness of what Socket.IO offers all the time. But honestly both are great and valid decisions. ws is nice because it _doesn't_ have a client; you just use the same `new WebSocket()` call we did on the client because that's more than enough for just WebSocket usage. However if you need all that retry logic, it's hard to beat Socket.IO. It's good for you to give this one a try too. 15 | 16 | ## HTTP2 Push 17 | 18 | Think of this as a one-way socket. Your client can connect to a server and the server can push many messages to the client. The difference here is that messages don't flow the opposite way: your client can't use the same connection to push messages back. In the terms of the app we just built, we could start a long-running HTTP2 connection and use that to get updates on new messages but just a normal RESTful POST back to the API to post a new message. Perfectly great architecture decision. 19 | 20 | ## WebRTC 21 | 22 | This is a similar idea but it's actually peer-to-peer rather than client-server. For our chat app, what would happen is a user would connect to the server and then the server would faciliate you connecting to your peers and then you would establish a connection with other chat users and send them messages directly without using the server as the middleman. 23 | 24 | ## SignalR 25 | 26 | Just wanted to shout out an emerging technology from Microsoft that I think is cool, even if it's not really in use for Node.js yet. [SignalR][signalr] is a realtime technology built for .NET but there have been rumblings of supporting platforms outside of .NET. In any case, it builds upon WebSockets (and occasionally will use them directly) to establish realtime communications. Think of it more as a competitor to Socket.IO where it's a protocol and library that uses various transports (HTTP, WebSockets, etc.) to do that. I was really impressed with it when I worked at Microsoft. 27 | 28 | ## Where to go from here 29 | 30 | So what now? I'd suggest either expanding upon our chat app and add features like a user system where users have to authenticate, direct/private messages, chat rooms, and some sort of durable storage like a database to store things. You could also make some sort of realtime drawing applications where many users can draw on one canvas in realtime. Try to flex those realtime muscles! You could also rebuild the frontend in your framework of choice like [React][react] to see how your preferred frameworks interacts with a realtime client. 31 | 32 | ## Congrats! 33 | 34 | Congrats again! Be sure to tweet at me and Frontend Masters and let us know how you did! 35 | 36 | [ws]: https://github.com/websockets/ws 37 | [signalr]: https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-5.0 38 | [react]: https://frontendmasters.com/courses/complete-react-v6/ 39 | -------------------------------------------------------------------------------- /lessons/images/FrontendMastersLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-realtime/432d60be586fb70a8e8ee7ceb67b89df06fc1c31/lessons/images/FrontendMastersLogo.png -------------------------------------------------------------------------------- /lessons/images/brian-beer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-realtime/432d60be586fb70a8e8ee7ceb67b89df06fc1c31/lessons/images/brian-beer.jpg -------------------------------------------------------------------------------- /lessons/images/brian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-realtime/432d60be586fb70a8e8ee7ceb67b89df06fc1c31/lessons/images/brian.jpg -------------------------------------------------------------------------------- /lessons/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/images/lunasit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-realtime/432d60be586fb70a8e8ee7ceb67b89df06fc1c31/lessons/images/lunasit.jpg -------------------------------------------------------------------------------- /lessons/intro-to-http2-push.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/intro-to-http2-push" 3 | title: "Intro to HTTP2 Push" 4 | order: "3A" 5 | description: "" 6 | section: "HTTP2" 7 | icon: "angle-double-right" 8 | --- 9 | 10 | So think of a normal HTTP request. I call `fetch("example.com")` and I wait for a single response to me. It's a one-to-one relationship. You make a request for a thing and you get back the thing. 11 | 12 | What if we could make an HTTP request, but then just not close the connection? That's the premise behind what we're going to do right now. It's a called a long-running HTTP call, or HTTP/2 Push. We're actually going to abuse it a bit for our purposes but I wanted to show you this is absolutely possible and could be a useful tool to you sometime. 13 | 14 | Let's chat a tiny second about HTTP's history. For a very long time (since '96) we've had HTTP 1.1 which is what we were just talking about: making a request and getting a response back. Simple and effective. Many sites out there still use HTTP 1.1 and there's little problem with that. It works and in my opinion it's still perfectly valid to use. 15 | 16 | After that came SPDY, a protocol developed by Google that made HTTP faster in a lot of ways. After a lot of browsers started shipping SPDY, it eventually got chosen to be the successor to HTTP 1.1 after nearly 20 years. 17 | 18 | In 2015 we got HTTP/2 ratified as the official new standard of HTTP. It added a bunch of new features, here a few highlights 19 | 20 | - We can now multiplex requests, meaning you can send many individual messages over a single connection. With 1.1 we had to do a whole new connection with headers, handshakes, security, etc. for every single request. With 2, you can reuse the same connection for multiple things. 21 | - Better compression strategies. Without getting into too much details, HTTP 2 allows for compression to happen at a finer grain details and thus allows better compression 22 | - Request prioritization. You can say some things are lower priority (like maybe images) and others are higher (like stylesheets.) 23 | 24 | There are others but let's dive into what we're here for, long running requests. 25 | 26 | In practice, this feature is primarily used to chunk up and send pages piecemeal. Imagine you have a document that takes a long time to render the body. What you can do is when a user of your site requests the page you can immediately send them the `` element because in the head you'll have all the CSS and font files that user will need to download and they can spend time downloading CSS and HTML while you rush and render everything in the body. You're streaming the content to them as soon as it's ready. Presumably your head is relatively immutable while the body is the dynamic part. Before, your user would have had to wait for the whole request to finish before they could even start downloading stuff referenced in the head. It's a very cool strategy and if you have any sort of long-rendering page you should immediately look into it. 27 | 28 | We're gonna abuse this system and just keep a connection open and send little JSON chunks. Let's do it. 29 | 30 | ## HTTP/3 31 | 32 | It has nothing to do with what we're doing today, but it's good to note HTTP/3 (aka QUIC) is coming. It messes less with the semantics of HTTP like 2 did and more with the transport of it. Whereas 1.1 and 2 used TCP to send data which has the fundamental problem that if you drop data, you have to wait for the packet to be sent again, making those stall painful. QUIC is based upon UDP which has a better recovery strategy and can accept packets out of order. There's lots more to read here, but that's the biggest change you can expect. 33 | -------------------------------------------------------------------------------- /lessons/intro-to-socketio.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/socketio" 3 | title: "Intro to Socket.IO" 4 | order: "5A" 5 | description: "" 6 | section: "Socket.IO" 7 | icon: "exchange-alt" 8 | --- 9 | 10 | Okay, so we got WebSockets working by hand, and we did it with no libraries. While a fun academic exercise, it's not a super practical approach to doing realtime communications. I'd say it's as practical as writing a Node.js server with zero libraries. Sure, the http library technically has all the building blocks you need but even something as Express can save you so much time in re-inventing the wheel. Beyond that, those core mechanics of adding middlewares and such are already well written and tested at scale. 11 | 12 | Similarly, with WebSockets, we _could_ write something that decodes frames by hands, handles all the connections, and all that by hand, but in the end we'll have a much larger surface area of code to maintain and bugfix for not much benefit. I'm even doubtful that you could eek out much performance benefit. 13 | 14 | In the end, you are _much_ better off using some sort of library to handle your WebSockets for you. There are a few super-high quality ones that are well maintained and already used at massive scales. There are a few options but two chief ones: [Socket.IO][io] and [ws][ws]. 15 | 16 | Let's chat a moment about ws. ws is a minimalist solution. It implements a clean, minmial take on WebSockets from a server perspective and then allows you to use the same WebSocket native object we were using in the browser for the first WebSocket example. It's a wonderful library and one you should definitely consider if you don't need the extra functionality that ships with Socket.IO. We are going to be talking about Socket.IO so you can get familiar with its functionality but know that ws is out there and a very good choice too. 17 | 18 | We are going to be talking about Socket.IO and only scratching its service. Socket.IO has a _bunch_ of functionality that it builds on top of sockets and can be super useful depending on your usecase 19 | 20 | - Managing of "rooms" or pools of connections. Think if you're building a Slack clone and you need multiple channels, you could use rooms to model people joining and leaving a room, but it could also be realms of a video game or multiple users collborating on a whiteboard 21 | - Be able to send and receive binary objects like photos, audio, and videos 22 | - If your user's browser doesn't support WebSockets, it'll fallback on long-polling! A very cool feature if you need to support devices with varying capabilities and it works seamlessly from the code perspective 23 | - Middlewares! Socket.IO has built-in support for adding middlewares so you can do things like auth, rate limiting, logging, and the like. It's compatibile with most existing connect-style middleware (aka Express middleware) so there's already a rich amount open source stuff you can use. 24 | - There are very cool libraries that will allow your clients to directly subscribe to data stores like MongoDB, Redis, PostgreSQL, and more with just a few lines of code. 25 | 26 | There's a lot more we could talk about. But for now let's go reimplement our project with Socket.IO. 27 | 28 | [ws]: https://github.com/websockets/ws 29 | [io]: https://socket.io 30 | -------------------------------------------------------------------------------- /lessons/intro-to-websockets.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/intro-to-websockets" 3 | title: "Intro to WebSockets" 4 | order: "4A" 5 | description: "" 6 | section: "WebSockets By Hand" 7 | icon: "hand-holding-heart" 8 | --- 9 | 10 | Let's get into true realtime: WebSockets. WebSockets are a primitive built into both browsers and backends alike that allow to us to have a long-running connection that allows clients to push data to servers and servers to push data to clients. As opposed to long-polling where we had a client that requesting and posting data to and from a server over many small connections, a WebSocket is one long-running connection where servers can push data to clients and vice versa. This is true realtime because it allows both sides to engage in realtime communication. 11 | 12 | So how do this connections work? We're going to implement a WebSocket connection by hand, getting down into the binary frames being sent back and forth between your server and the browser. Why? You would never actually write this code yourself. But, just like [in my containers course][containers] where we build containers by hand to see how they actually work, we are actually going to build it so we can understand what underpins the technology. While you may never need to build it yourself, it will make you a better programmer to know how the tool works and you'll be grateful for the abstraction when you use it because you'll understand the complexity it is shielding you from. 13 | 14 | So let's begin by getting our chat app working with sockets we build by hand. 15 | 16 | [containers]: https://frontendmasters.com/courses/complete-intro-containers/ 17 | -------------------------------------------------------------------------------- /lessons/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/intro" 3 | title: "Introduction" 4 | order: "1A" 5 | section: "Welcome" 6 | description: "this is the description that will show up in social shares" 7 | icon: "door-open" 8 | --- 9 | 10 | Hello! And welcome to the Complete Intro to Realtime. 11 | 12 | > An easy to link to this site is [bit.ly/intro-realtime][bitly] (case sensitive) 13 | 14 | In this course we're going to go over the two primary ways of doing realtime communication between a client (a browser) and a server (your backend). If you've ever needed to sync state across multiple devices, this is the course for you. We'll go over the two primary ways of doing this, long-polling and WebSockets. This course aims to take a first-principles approach: we will go over the lowest level details we can and build our way up to the higher level abstractions. The reason why we do this is that I want you to value your tools. I've found that if I don't experience the problem that a tool solves for me, I don't know how to use the tool as well and I frequently resist the complexity the tool introduces. That's what was abstractions frequently trade-off for you: complexity and performance in exchange for ease and velocity. You and will build a long-polling system and a WebSocket tool by hand before introducing you to Socket.IO which will cover all of it for you. 15 | 16 | ## Who are you 17 | 18 | In order to best grasp this course this should not be your first JavaScript course. If you need to learn JavaScript or programming in general, Frontend Masters definitely has you covered. [Check out the free Boot Camp][bc] (taught by [Jen Kramer][jen] and me) or the [Intro to Web Dev v2][web] (taught by me) and get up to speed before you hop into this one. It'd be great if this wasn't your first exposure to Node.js too. If you need help with that, check out [Scott Moss's fantastic course][scott]. 19 | 20 | ## Who am I 21 | 22 |  23 | 24 | Hi! My name is Brian Holt and I developed this course. I'm currently the product manager of developer products at [Stripe][stripe] which means I help create and maintain all the tools developers use to integrate with Stripe. I've been a developer, advocate, and PM during my career at companies like Reddit, Netflix, LinkedIn, Microsoft, and now Stripe. I absolutely love to teach and have now taught [many courses at Frontend Masters][courses]. All these things we'll be talking about today are things I've done before in production. I did a lot of realtime stuff at Reddit and Netflix in particular. 25 | 26 |  27 | 28 | I currently live in Seattle, Washington and I love it. When not working, you'll find me hanging out with my little family and Havanese dog Luna, exercising on my Peloton, playing Dota 2 and Overwatch poorly, and sampling the finest Islay Scotches and local hazy IPAs and medium roast coffees. I absolutely love to travel, speak fluent Italian, and meeting new friends. 29 | 30 |  31 | 32 | Feel free to catch up with my / add me on these social medias. I'm really bad at responding to DMs! 33 | 34 | - [Twitter][twitter] 35 | - [GitHub][gh] 36 | - [LinkedIn][li] 37 | - [Peloton][pelo] 38 | 39 | ## Where to File Issues 40 | 41 | I write these courses and take care to not make mistakes. However when teaching over ten hours of material, mistakes are inevitable, both here in the grammar and in the course with the material. However I (and the wonderful team at Frontend Masters) are constantly correcting the mistakes so that those of you that come later get the best product possible. If you find a mistake we'd love to fix it. The best way to do this is to [open a pull request or file an issue on the GitHub repo][issues]. While I'm always happy to chat and give advice on social media, I can't be tech support for everyone. And if you file it on GitHub, those who come later can Google the same answer you got. 42 | 43 | ## Special Thanks to Frontend Masters 44 | 45 |  46 | 47 | I want to thank Marc and the whole Frontend Masters team explicitly. In addition to being family to me these are some of the most wonderful people I've ever met. You are reading or watching this course thanks to their hard work to make the world of tech more approachable with high quality instructors teaching what they know best. I want to thank them for creating the platform, garnering a community of knowledge-seeking developers, and giving me incentive and a platform to speak to you all. One specific kindness is that while the videos are on the platform (and I think they are worth every penny to watch) they let me release this website and materials as open source so every person can acquire the knowledge. 48 | 49 | Thanks Frontend Masters. Y'all are the best. 50 | 51 | > [Please star the repo! ⭐️][repo] 52 | 53 | [bitly]: https://bit.ly/intro-realtime 54 | [bc]: https://frontendmasters.com/bootcamp/ 55 | [web]: https://frontendmasters.com/courses/web-development-v2/ 56 | [scott]: https://frontendmasters.com/courses/node-js-v2/ 57 | [stripe]: https://stripe.com/ 58 | [courses]: https://frontendmasters.com/teachers/brian-holt/ 59 | [twitter]: https://twitter.com/holtbt 60 | [gh]: https://github.com/btholt 61 | [li]: https://www.linkedin.com/in/btholt/ 62 | [pelo]: https://members.onepeloton.com/members/btholt/overview 63 | [issues]: https://github.com/btholt/complete-intro-to-realtime/issues 64 | [repo]: https://github.com/btholt/complete-intro-to-realtime 65 | -------------------------------------------------------------------------------- /lessons/polling-backend.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/polling-backend" 3 | title: "Polling Backend" 4 | order: "2B" 5 | section: "Polling" 6 | description: "" 7 | --- 8 | 9 | > We are going to implement our chat app using long-polling. Open your app to the [polling/exercise directory][gh] and we'll do this together. 10 | 11 | Let's do the backend first. Open backend/server.js. Let's acquaint ourselves with the code here. 12 | 13 | - If you haven't already, please run npm install in the root of the project 14 | - The backend is done with Express.js. I chose this because nearly every Node.js dev has some familiarity with Express and this isn't a Node.js course. Take Scott Moss's amazing Node.js course if you want more Node.js goodness. 15 | - body-parser allows Express to parse request bodies from the browser 16 | - nanobuffer allows us to create capped collection. Our array will only store the last 50 messages and drop them off the end. We're using this instead of database. This could be written more performantly but I erred on the side of simplicity. 17 | - morgan is a logging library so we can see some nice request logs 18 | - All the frontend code is being served by the `express.static` call 19 | 20 | All the bones of what we need to do are done. We just need to implement the get and the post. We put them on the same URL endpoint but that isn't required. For now the semantic differences of post and get are enough for our little app. 21 | 22 | If you run `npm run dev` it will start the development server with nodemon. This means everytime you save you will automatically restart your server so you can immediately see changes. Do note since all the messages are being stored in memory that it will drop your chat record. That's to be expected; normally you'd store them in a database. 23 | 24 | Okay, let's do our get first. 25 | 26 | ```javascript 27 | // replace get 28 | app.get("/poll", function (req, res) { 29 | res.json({ 30 | msg: getMsgs(), 31 | }); 32 | }); 33 | ``` 34 | 35 | This is part of the charm of long-polling: it's just normal API requests done frequently. This is a very normal get request. 36 | 37 | Now have your browser or API request client (like Postman or Insomnia) hit your http://localhost:3000/poll endpoint and see if it works! You should see a single message come back. 38 | 39 | Okay, the post is pretty similar here, so let's go do that. 40 | 41 | ```javascript 42 | // replace post 43 | app.post("/poll", function (req, res) { 44 | const { user, text } = req.body; 45 | 46 | currentId++; 47 | msg.push({ 48 | user, 49 | text, 50 | time: Date.now(), 51 | }); 52 | 53 | res.json({ 54 | status: "ok", 55 | }); 56 | }); 57 | ``` 58 | 59 | Awesome! Now try using something like Insomnia to make a post to your new end point with a user and a text and see if it shows up in the get. 60 | 61 | [gh]: https://github.com/btholt/complete-intro-to-realtime 62 | -------------------------------------------------------------------------------- /lessons/polling-with-settimeout.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/settimeout" 3 | title: "Polling with setTimeout" 4 | order: "2C" 5 | description: "" 6 | section: "Polling" 7 | --- 8 | 9 | The frontend is more where the interesting part of long-polling lives. At first let's do it with a simple setTimeout to get our response back. 10 | 11 | Open polling-chat.js in the frontend directory. 12 | 13 | ```javascript 14 | // replace getNewMessages 15 | async function getNewMsgs() { 16 | let json; 17 | try { 18 | const res = await fetch("/poll"); 19 | json = await res.json(); 20 | } catch (e) { 21 | // back off code would go here 22 | console.error("polling error", e); 23 | } 24 | allChat = json.msg; 25 | render(); 26 | setTimeout(getNewMsgs, INTERVAL); 27 | } 28 | 29 | // just notice this is the last line of the doc 30 | getNewMsgs(); 31 | ``` 32 | 33 | This is what does the heavy lifting for us. This is what will hit that endpoint every 3 seconds (well, 3 seconds plus however long the request takes). 34 | 35 | Again, the temptation here would be to just use setInterval but this strategy is superior because it will make sure only one request is ever in flight. With setInterval, it will request every X seconds, no matter what. Let's knock out the post request too. 36 | 37 | ```javascript 38 | async function postNewMsg(user, text) { 39 | const data = { 40 | user, 41 | text, 42 | }; 43 | 44 | // request options 45 | const options = { 46 | method: "POST", 47 | body: JSON.stringify(data), 48 | headers: { 49 | "Content-Type": "application/json", 50 | }, 51 | }; 52 | 53 | // send POST request 54 | const res = await fetch("/poll", options); 55 | const json = await res.json(); 56 | } 57 | ``` 58 | 59 | Nothing too special here: just a normal ol' post request. 60 | 61 | Now your app should be working for long polling! This is the simplest way to acheive realtime: just make a lot of requests! Let's take this a step further and try to make it a bit better. 62 | 63 | > The current state of the repo can be found in the [no-pause][gh] directory on the project. 64 | 65 | [gh]: https://github.com/btholt/realtime-exercises/tree/main/polling/no-pause 66 | -------------------------------------------------------------------------------- /lessons/polling.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/polling" 3 | title: "Intro to Long-Polling" 4 | order: "2A" 5 | section: "Polling" 6 | description: "" 7 | icon: "spinner" 8 | --- 9 | 10 | We are going to write a chat app where anyone can connect to a URL and begin chatting about anything. How would you go about architecting how to build this sort of app? 11 | 12 | Let's think about the product requirements. 13 | 14 | 1. A user needs to be able to post new messages 15 | 1. A user needs to be able to see old messages from the chat when they first connect 16 | 1. A user needs to be able to see their own messages 17 | 1. A user needs to be able to see new messages posted by other people 18 | 19 | As you may imagine, there are many ways to architect this system and some work better in some ways and worse in others. In other words, there are trade-offs. We're going to start with perhaps the simplest approach to this problem: the humble long-poll. 20 | 21 | ## Long Polling 22 | 23 | Long polling (to which I'll refer as just polling from here on out) is really a way of saying "making a lot of requests." There's no special technology here, it's just making an AJAX call on some interval. 24 | 25 | What are some of the keys here? 26 | 27 | The polling end point is going to get called a lot, so make sure it's designed that way. If you have a 100 clients making requests every 3 seconds, that that's 2,000 requests a minute, and it just scales from there. Try to make it as fast as possible and offload all other actions to be done outside of the hot path. 28 | 29 | Don't just use `setInterval` as tempting as that sounds. setInterval sets some function to be set off every X seconds and doesn't account for the fact that a request takes time. What if you're making a request every 3 seconds but your API takes 4 seconds to respond? Now you're making requests while others are still in flight. Instead, what you want to do is start a timer for the next request as soon as the current one completes. `setTimeout` at the end of your function with the next time for the next request is one way to do this. 30 | -------------------------------------------------------------------------------- /lessons/requestanimationframe.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/requestanimationframe" 3 | title: "Polling with requestAnimationFrame" 4 | order: "2D" 5 | description: "" 6 | section: "Polling" 7 | --- 8 | 9 | What if a user unfocuses the window? Either focuses another tab or opens Spotify. Do you want to still be making requests in the background? That's a valid product question and one you'd need to make a deliberate answer to. If your user is trying to buy concert tickets and they're going to sell out then you absolutely should run in the background to give your user the best shot of getting the tickets. If it's a realtime weather app, it's probably okay to pause your polling and wait for the user to refocus again so we don't waste data and resources of the user's device, especially if they're just going to close the tab and never look at it again. I don't know about you, but lots of people have 10 million Chrome tabs open at once and it could waste a lot of data if they're all doing polling in the background. 10 | 11 | If you want to not pause when unfocused, setTimeout is a good way. If you do want to pause, requestAnimationFrame will automatically pause when the window isn't in use. In general, when on the fence, I'd prefer the latter as a better implementation. 12 | 13 | The difference in code is actually surprisingly small too. Let's go knock it out. 14 | 15 | ```javascript 16 | // delete the following line from getNewMessage 17 | setTimeout(getNewMsgs, INTERVAL); 18 | 19 | // replace the final getNewMesgs call at the bottom 20 | let timeToMakeNextRequest = 0; 21 | async function rafTimer(time) { 22 | if (timeToMakeNextRequest <= time) { 23 | await getNewMsgs(); 24 | timeToMakeNextRequest = time + INTERVAL; 25 | } 26 | requestAnimationFrame(rafTimer); 27 | } 28 | 29 | requestAnimationFrame(rafTimer); 30 | ``` 31 | 32 | getNewMessages doesn't change other than deleting the one line. The post doesn't change either. 33 | 34 | Let's chat about requestAnimationFrame. It runs _a lot_. But the good thing is it only runs when the main JS thread is idle, guaranteeing you're not interrupting any repaints. setInterval and setTimeout are hammers: they run and will absolutely interrupt any code execution and paints that happening. For our previous example it's probably not a big deal: our code was fairly lightweight and not really heavy enough to ever cause much issue. But when dealing with a more animated frontend or any sort of heavier app this could be a problem that requestAnimationFrame can help. 35 | 36 | So requestAnimationFrame runs whenever the thread is idle. That's a lot. So that core function rafTimer needs to be super lightweight. Just check if it's time to run and if not just schedule itself to run again. That's all. We get the background pausing feature for free. Notice now if you put your tab in the background and wait a few minutes it will eventually stop polling until your bring it back up. Every browser is different before it sleeps a tab but it could be a few minutes. I think Firefox will actually resume the tab if you even just hover over the tab. Cool stuff, but it's nice to offload all that work to the browser. 37 | 38 | That's it! That's really the basics of just doing long-polling. 39 | 40 | > The current state of the repo can be found in the [pause-on-unfocus][gh] directory on the project. 41 | 42 | [gh]: https://github.com/btholt/realtime-exercises/tree/main/polling/pause-on-unfocus 43 | -------------------------------------------------------------------------------- /lessons/the-project.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/the-project" 3 | title: "The Project" 4 | order: "1B" 5 | description: "" 6 | section: "Welcome" 7 | --- 8 | 9 | This course works and has been tested on both macOS and Windows 10. It also will work very well on Linux (just follow the macOS instructions). You shouldn't need a particularly powerful computer for any part of this course. 8GB of RAM would more than get you through it and you can definitely get away with less. 10 | 11 | - Install Node.js. Make sure your version of Node.js is at least 12, preferably the latest stable release. I prefer using nvm to install Node.js, [see setup instructions here][nvm]. 12 | - While you do not have to use [Visual Studio Code][vsc], it is what I will be using and I'll be giving you fun tips for it along the way. I am on the VS Code team so I'm a bit biased! 13 | - People often ask me what my coding set up is so let's go over that really quick! 14 | - Font: [Dank Mono][dank]. Be sure to [enable ligatures][ligatures] in VS Code! If you want ligatures without Dank, check out Microsoft's [Cascadia Code][cascadia]. 15 | - Theme: I actually just like Dark+, the default VS Code theme. Though I do love [Sarah Drasner's Night Owl][night-owl] too. 16 | - Terminal: I just switched back to using macOS's built in terminal. [iTerm2][iterm] is great too. On Windows I love [Windows Terminal][terminal]. 17 | - VS Code Icons: the [vscode-icons][icons] extension. 18 | 19 | ## The exercises 20 | 21 | [Fork and clone the btholt/realtime-exercises][exercises] repo. I have you fork it so you can keep track of your own changes. 22 | 23 | Each of the subdirectories are a self-contained project. This means to start anyone of them, you'll need to run npm install to set up their dependencies. Let's chat a few things about the project. 24 | 25 | - The Node.js parts do have dependencies so you will need to npm install 26 | - Part of the course is done with Express. This is just to make it as real-world for you as possible since most people use some server-side framework 27 | - Where I had a choice between doing things "right" versus "easy to quickly understand", I chose the latter. I tried to make it so you could just focus on the realtime portion of the course and hopefully not stumble on things not related to the course. This means the frontend and backend code are not coded with production in mind and are inefficient for the sake of being simple. 28 | - Particularly with the front end code, it doesn't use any framework and takes an intentionally simplistic rendering scheme. It's very inefficient, but it's very simple. It suits our needs and doesn't require you to understand any framework, 29 | - There is no frontend build process. I did this to maintain simplicity. All the dependencies are being loaded from a CDN. I don't suggest doing this in production but it made for the simplest process for all of us to get started and to have the least amount stumbling stones. 30 | - For all the projects, npm run dev will run a dev process that will restart Node.js on every save and npm run start will just start the server without the automatic reload 31 | - Since all the messages being stored in memory, every time you restart the server you'll lose your message history. You'd need to either write to a file or a database to keep it across reloads 32 | - The CSS and UI code is minimal so we can focus on realtime. It's using [Materialize][materialize] as a base 33 | 34 | Let's get started! 35 | 36 | [nvm]: https://github.com/nvm-sh/nvm 37 | [vsc]: https://code.visualstudio.com/ 38 | [dank]: https://gumroad.com/l/dank-mono 39 | [ligatures]: https://worldofzero.com/posts/enable-font-ligatures-vscode/ 40 | [night-owl]: https://marketplace.visualstudio.com/items?itemName=sdras.night-owl 41 | [cascadia]: https://github.com/microsoft/cascadia-code 42 | [terminal]: https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701?activetab=pivot:overviewtab 43 | [icons]: https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons 44 | [iterm]: https://iterm2.com/ 45 | [exercises]: https://github.com/btholt/realtime-exercises 46 | [materialize]: https://materializecss.com/ 47 | -------------------------------------------------------------------------------- /lessons/websockets-backend.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: "/websockets-backend" 3 | title: "WebSockets Backend" 4 | order: "4B" 5 | description: "" 6 | section: "WebSockets By Hand" 7 | --- 8 | 9 | Go ahead and open in our project the websockets/exercise-raw folder. In here you'll find a relatively similar project to the other one we were working on: a Chat with Me app where multiple anonymous users can hop on and have a chat. 10 | 11 | A first warning here: we are intentionally doing an imperfect, incomplete implementation here. Most important is that you learn what a websocket is and how it works. Then we'll use a battle-tested library that will handle the rest of it for us. 12 | 13 | Okay, let's do a little thing in our frontend app first. Open raw-chat.js and put this 14 | 15 | ```javascript 16 | // under postNewMessage 17 | 18 | const ws = new WebSocket("ws://localhost:8080", ["json"]); 19 | 20 | ws.addEventListener("open", () => { 21 | console.log("connected"); 22 | presence.innerText = "🟢"; 23 | }); 24 | ``` 25 | 26 | This will attempt to make a connection from our browser to our server. Once it does, it will log out in the console it's connected and update the little circle in the top right that it's connected. If you try to run it right now you'll see a browser error similar to `Firefox can’t establish a connection to the server at ws://localhost:8080/.` 27 | 28 | We'll talk about the "json" portion below, but know it's the client saying "hey, I want to speak to JSON with you". 29 | 30 | Cool, now we can start building a server to handle having a socket connection. It's actually really interesting process. We are going to first just try to receive _any_ connection from anyone. That's how sockets work: they make a normal TCP/IP connection but after that connection is established, it requests an **upgrade** to a socket connection. So we can actually use a similar methodology we did in the first exercise to accept that request. In backend/server.js put: 31 | 32 | ```javascript 33 | // under server creation 34 | server.on("upgrade", function (req, socket) { 35 | if (req.headers["upgrade"] !== "websocket") { 36 | // we only care about websockets 37 | socket.end("HTTP/1.1 400 Bad Request"); 38 | return; 39 | } 40 | 41 | console.log("upgrade requested!"); 42 | }); 43 | ``` 44 | 45 | Try running your server with `npm run dev` and opening your browser. You should see your server log out "upgrade requested!". This means your browser connected and then requested your connection be upgraded from a normal connection to a websocket. In theory there are other types of upgrades other than websockets, I just don't know what they are. In any case, we only care about websockets so we'll reject anything that isn't with a 400. 46 | 47 | Okay, so next part is we need to have the server and client negotiate and verify with each other that indeed they both want to do a websocket connection. The way they do this is the client (the browser) send an acceptance key in the headers (the browser does this automatically for you when you say `new WebSocket`) and the browser needs to create a hash that mixes the key the browser sent with the key `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`. Why that key? It's a randomly generated key that's hardcoded into the WebSocket spec. 48 | 49 | Why do we do this? It's _not_ for security as some of you may suspect (as I did too.) There's actually a HTTPS version of WebSockets that's wss:// instead of ws:// and that is what provides the encryption (we're not doing that today.) No, actually, the reason why we do this is because we don't either the client nor the server to mistake each other for wanting to do a WebSocket connection. This is meant so that you can't just make a REST request to a WebSocket endpoint accidentally (or intentionally by a bad actor.) By doing this handshake we're assuring that both parties know they're about to engage in a WebSocket connection. 50 | 51 | Okay, let's do that. I already made `generate-accept-value.js` for you so you just need to use the imported function. We're going to use that to write some headers back to the client so we can establish a firm connection between browser and client. 52 | 53 | ```javascript 54 | // under if (req.headers["upgrade"] !== "websocket") { … } 55 | // inside the on("upgrade") 56 | 57 | const acceptKey = req.headers["sec-websocket-key"]; 58 | const acceptValue = generateAcceptValue(acceptKey); 59 | const headers = [ 60 | "HTTP/1.1 101 Web Socket Protocol Handshake", 61 | "Upgrade: WebSocket", 62 | "Connection: Upgrade", 63 | `Sec-WebSocket-Accept: ${acceptValue}`, 64 | "Sec-WebSocket-Protocol: json", 65 | "\r\n", 66 | ]; 67 | 68 | socket.write(headers.join("\r\n")); 69 | ``` 70 | 71 | Now refresh your browser. Notice the presence indicator in the top right says you're connected! In your console you should see a connected log as well. We now have an open connection that we're just leaving open. 72 | 73 | Let's talk about the headers. Most of those should make sense but let's about the last two. The Protocol allows the client and server to work out how the data is going to be formatted going back and forth. At the beginning when we said `const ws = new WebSocket("ws://localhost:8080", ["json"]);`, the array is a list of protocols the client is saying it can handle. In this case, we're just saying "we only want json". We could have said "we can support json and xml." No matter what, the server will send back _one_ protocol it will be speaking. There's no set rules of what protocols can be used here. All that matters is that you settle on one by the end. 74 | 75 | The last double `\r\n` (there's two because we do a join which means the last two lines sent are `\r\n\r\n`) is how we signify that we're done sending headers and now we'll be sending data. 76 | 77 | Okay! So now we have an open socket. On the server side we can say `socket.write` and it'll send data to the client and on the client we can say `ws.send` to send data to the server. Let's go ahead and finish our server implementation and then we'll cover the rest of the client. 78 | 79 | First thing, on connection let's immediately write out the state of the chat (similar to how we did previously.) 80 | 81 | ```javascript 82 | // under write headers 83 | socket.write(objToResponse({ msg: getMsgs() })); 84 | ``` 85 | 86 | This will immediately on connection send the client the history of the chats. Let's display that information. Head back to raw-chat.js 87 | 88 | ```javascript 89 | // under open event 90 | ws.addEventListener("message", (event) => { 91 | const data = JSON.parse(event.data); 92 | allChat = data.msg; 93 | render(); 94 | }); 95 | ``` 96 | 97 | This receives any message from the server, saves, it and calls render. When doing sockets, you may do different sorts of messages (think pub/sub if you've tried that before) and here's where you would route different messages to different functions. In our case, we're just listening for new message lists, which we'll deal with here. 98 | 99 | Now let's listen for data from the client. In the server.js put 100 | 101 | ```javascript 102 | // under write objToResponse 103 | socket.on("data", (buffer) => { 104 | console.log(buffer); 105 | }); 106 | ``` 107 | 108 | and in the raw-chat.js put: 109 | 110 | ```javascript 111 | // replace postNewMsg 112 | async function postNewMsg(user, text) { 113 | const data = { 114 | user, 115 | text, 116 | }; 117 | 118 | ws.send(JSON.stringify(data)); 119 | } 120 | ``` 121 | 122 | Okay, so now submit a new message to from the web page to server. You'll probably see something like `You just hit a route that doesn't exist... the sadness.
7 |