diff --git a/1-js/11-async/07-microtask-queue/article.md b/1-js/11-async/07-microtask-queue/article.md index 30d14beb..1c95e064 100644 --- a/1-js/11-async/07-microtask-queue/article.md +++ b/1-js/11-async/07-microtask-queue/article.md @@ -126,13 +126,62 @@ Naturally, `promise` shows up first, because `setTimeout` macrotask awaits in th So call have a promise chain that doesn't wait for anything, then things like `setTimeout` or event handlers can never get in the middle. + +## Unhandled rejection + +Remember "unhandled rejection" event from the chapter ? + +Now, with the understanding of microtasks, we can formalize it. + +**"Unhandled rejection" is when a promise error is not handled at the end of the microtask queue.** + +For instance, consider this code: + +```js run +let promise = Promise.reject(new Error("Promise Failed!")); + +window.addEventListener('unhandledrejection', event => { + alert(event.reason); // Promise Failed! +}); +``` + +We create a rejected `promise` and do not handle the error. So we have the "unhandled rejection" event (printed in browser console too). + +We wouldn't have it if we added `.catch`, like this: + +```js run +let promise = Promise.reject(new Error("Promise Failed!")); +*!* +promise.catch(err => alert('caught')); +*/!* + +// no error, all quiet +window.addEventListener('unhandledrejection', event => alert(event.reason)); +``` + +Now let's say, we'll be catching the error, but after an extremely small delay: + +```js run +let promise = Promise.reject(new Error("Promise Failed!")); +*!* +setTimeout(() => promise.catch(err => alert('caught')), 0); +*/!* + +// Error: Promise Failed! +window.addEventListener('unhandledrejection', event => alert(event.reason)); +``` + +Now the unhandled rejction appears again. Why? Because `unhandledrejection` triggers when the microtask queue is complete. The engine examines promises and, if any of them is in "rejected" state, then the event is generated. + +In the example above `setTimeout` adds the `.catch`, and it triggers too, of course it does, but later, after the event has already occured. + ## Summary - Promise handling is always asynchronous, as all promise actions pass through the internal "promise jobs" queue, also called "microtask queue" (v8 term). **So, `.then/catch/finally` is called after the current code is finished.** - If we need to guarantee that a piece of code is executed after `.then/catch/finally`, it's best to add it into a chained `.then` call. + If we need to guarantee that a piece of code is executed after `.then/catch/finally`, it's best to add it into a chained `.then` call. - There's also a "macrotask queue" that keeps various events, network operation results, `setTimeout`-scheduled calls, and so on. These are also called "macrotasks" (v8 term). diff --git a/6-binary/01-arraybuffer-and-views/01-concat/_js.view/solution.js b/6-binary/01-arraybuffer-binary-arrays/01-concat/_js.view/solution.js similarity index 100% rename from 6-binary/01-arraybuffer-and-views/01-concat/_js.view/solution.js rename to 6-binary/01-arraybuffer-binary-arrays/01-concat/_js.view/solution.js diff --git a/6-binary/01-arraybuffer-and-views/01-concat/_js.view/source.js b/6-binary/01-arraybuffer-binary-arrays/01-concat/_js.view/source.js similarity index 100% rename from 6-binary/01-arraybuffer-and-views/01-concat/_js.view/source.js rename to 6-binary/01-arraybuffer-binary-arrays/01-concat/_js.view/source.js diff --git a/6-binary/01-arraybuffer-and-views/01-concat/_js.view/test.js b/6-binary/01-arraybuffer-binary-arrays/01-concat/_js.view/test.js similarity index 100% rename from 6-binary/01-arraybuffer-and-views/01-concat/_js.view/test.js rename to 6-binary/01-arraybuffer-binary-arrays/01-concat/_js.view/test.js diff --git a/6-binary/01-arraybuffer-and-views/01-concat/solution.md b/6-binary/01-arraybuffer-binary-arrays/01-concat/solution.md similarity index 100% rename from 6-binary/01-arraybuffer-and-views/01-concat/solution.md rename to 6-binary/01-arraybuffer-binary-arrays/01-concat/solution.md diff --git a/6-binary/01-arraybuffer-and-views/01-concat/task.md b/6-binary/01-arraybuffer-binary-arrays/01-concat/task.md similarity index 100% rename from 6-binary/01-arraybuffer-and-views/01-concat/task.md rename to 6-binary/01-arraybuffer-binary-arrays/01-concat/task.md diff --git a/6-binary/01-arraybuffer-and-views/8bit-integer-256.png b/6-binary/01-arraybuffer-binary-arrays/8bit-integer-256.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/8bit-integer-256.png rename to 6-binary/01-arraybuffer-binary-arrays/8bit-integer-256.png diff --git a/6-binary/01-arraybuffer-and-views/8bit-integer-256@2x.png b/6-binary/01-arraybuffer-binary-arrays/8bit-integer-256@2x.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/8bit-integer-256@2x.png rename to 6-binary/01-arraybuffer-binary-arrays/8bit-integer-256@2x.png diff --git a/6-binary/01-arraybuffer-and-views/8bit-integer-257.png b/6-binary/01-arraybuffer-binary-arrays/8bit-integer-257.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/8bit-integer-257.png rename to 6-binary/01-arraybuffer-binary-arrays/8bit-integer-257.png diff --git a/6-binary/01-arraybuffer-and-views/8bit-integer-257@2x.png b/6-binary/01-arraybuffer-binary-arrays/8bit-integer-257@2x.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/8bit-integer-257@2x.png rename to 6-binary/01-arraybuffer-binary-arrays/8bit-integer-257@2x.png diff --git a/6-binary/01-arraybuffer-and-views/arraybuffer-view-buffersource.png b/6-binary/01-arraybuffer-binary-arrays/arraybuffer-view-buffersource.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/arraybuffer-view-buffersource.png rename to 6-binary/01-arraybuffer-binary-arrays/arraybuffer-view-buffersource.png diff --git a/6-binary/01-arraybuffer-and-views/arraybuffer-view-buffersource@2x.png b/6-binary/01-arraybuffer-binary-arrays/arraybuffer-view-buffersource@2x.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/arraybuffer-view-buffersource@2x.png rename to 6-binary/01-arraybuffer-binary-arrays/arraybuffer-view-buffersource@2x.png diff --git a/6-binary/01-arraybuffer-and-views/arraybuffer-views.png b/6-binary/01-arraybuffer-binary-arrays/arraybuffer-views.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/arraybuffer-views.png rename to 6-binary/01-arraybuffer-binary-arrays/arraybuffer-views.png diff --git a/6-binary/01-arraybuffer-and-views/arraybuffer-views@2x.png b/6-binary/01-arraybuffer-binary-arrays/arraybuffer-views@2x.png similarity index 100% rename from 6-binary/01-arraybuffer-and-views/arraybuffer-views@2x.png rename to 6-binary/01-arraybuffer-binary-arrays/arraybuffer-views@2x.png diff --git a/6-binary/01-arraybuffer-and-views/article.md b/6-binary/01-arraybuffer-binary-arrays/article.md similarity index 99% rename from 6-binary/01-arraybuffer-and-views/article.md rename to 6-binary/01-arraybuffer-binary-arrays/article.md index abba641e..140a33e3 100644 --- a/6-binary/01-arraybuffer-and-views/article.md +++ b/6-binary/01-arraybuffer-binary-arrays/article.md @@ -1,4 +1,4 @@ -# ArrayBuffer and views +# ArrayBuffer, binary arrays Binary data appears when we work with arbitrary files (uploading, downloading, creation). Or when we want to do image/audio processing. diff --git a/6-binary/02-text-decoder/article.md b/6-binary/02-text-decoder/article.md index e264a110..8778077a 100644 --- a/6-binary/02-text-decoder/article.md +++ b/6-binary/02-text-decoder/article.md @@ -1,4 +1,4 @@ -# TextDecoder, TextEncoder +# TextDecoder and TextEncoder What if the binary data is actually a string? diff --git a/6-binary/04-file/article.md b/6-binary/04-file/article.md index 3c71322e..64a1edab 100644 --- a/6-binary/04-file/article.md +++ b/6-binary/04-file/article.md @@ -102,5 +102,19 @@ Reading methods `read*` do not generate events, but rather return the result, as That's only inside a Web Worker though, because delays and hang-ups in Web Workers are less important, they do not affect the page. ``` +## Summary -It most often used to read from files, and +`File` object inherit from `Blob`. + +In addition to `Blob` methods and properties, `File` objects also have `fileName` and `lastModified` properties, plus the internal ability to read from filesystem. We usually get `File` objects from user input, like `` or drag'n'drop. + +`FileReader` objects can read from a file or a blob, in one of three formats: +- String (`readAsText`). +- `ArrayBuffer` (`readAsArrayBuffer`). +- Data url, base-64 encoded (`readAsDataURL`). + +In many cases though, we don't have to read the file contents. + +We can create a blob url with `URL.createObjectURL(file)` and assign it to `` or ``. This way the file can be downloaded or show up as an image, as a part of canvas etc. + +And if we're going to send a `File` over a network, then it's also easy, as network API like `XMLHttpRequest` or `fetch` natively accepts `File` objects. diff --git a/7-network/1-fetch-basics/article.md b/7-network/1-fetch-basics/article.md index 1d87d894..38307d8c 100644 --- a/7-network/1-fetch-basics/article.md +++ b/7-network/1-fetch-basics/article.md @@ -16,39 +16,17 @@ let promise = fetch(url, [params]) The browser starts the request right away and returns a `promise`. -Accepting a response is usually a two-step procedure. +Accepting a response is usually a two-stage process. -**The `promise` resolves as soon as the server responded with headers.** +**The `promise` resolves with an object of the built-in [Response](https://fetch.spec.whatwg.org/#response-class) class as soon as the server responds with headers.** -So we can access the headers, we know HTTP status, whether the response is successful, but don't have the body yet. - -We need to wait for the response body additionally, like this: - -```js run async -let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits'); -*!* -let commits = await response.json(); -*/!* -alert(commits[0].author.login); -``` - -Or, using pure promises: - -```js run -fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits') - .then(response => response.json()) - .then(commits => alert(commits[0].author.login)); -``` - -A `fetch` resolves with `response` -- an object of the built-in [Response](https://fetch.spec.whatwg.org/#response-class) class. +So we can access the headers, we know HTTP status, whether it is successful, but don't have the body yet. The main response properties are: - **`ok`** -- boolean, `true` if the HTTP status code is 200-299. - **`status`** -- HTTP status code. - **`headers`** -- HTTP headers, a Map-like object. -## How to get headers? - We can iterate over headers the same way as over a `Map`: ```js run async @@ -67,25 +45,41 @@ if (response.ok) { } ``` -## How to get response? +To get the response body, we need to use an additional method call. -`Response` allows to access the body in multiple formats, using following promises: +`Response` allows to access the body in multiple formats, using following promise-based methods: - **`json()`** -- parse as JSON object, - **`text()`** -- as text, - **`formData()`** -- as formData (form/multipart encoding), - **`blob()`** -- as Blob (for binary data), -- **`arrayBuffer()`** -- as ArrayBuffer (for binary data), -- `response.body` is a [ReadableStream](https://streams.spec.whatwg.org/#rs-class) object, it allows to read the body chunk-by-chunk. +- **`arrayBuffer()`** -- as ArrayBuffer (for binary data) +- additionally, `response.body` is a [ReadableStream](https://streams.spec.whatwg.org/#rs-class) object, it allows to read the body chunk-by-chunk. -We already saw how to get the response as json. +For instance, here we get the response as JSON: -As text: +```js run async +let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits'); +*!* +let commits = await response.json(); +*/!* +alert(commits[0].author.login); +``` + +Or, using pure promises: + +```js run +fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits') + .then(response => response.json()) + .then(commits => alert(commits[0].author.login)); +``` + +To get text: ```js let text = await response.text(); ``` -For the binary example, let's download an image and show it: +And for the binary example, let's fetch and show an image (see chapter [Blob](info:blob) for details about operations on blobs): ```js async run let response = await fetch('/article/fetch/logo-fetch.svg'); @@ -94,172 +88,22 @@ let response = await fetch('/article/fetch/logo-fetch.svg'); let blob = await response.blob(); // download as Blob object */!* -// create with it +// create for it let img = document.createElement('img'); +img.style = 'position:fixed;top:10px;left:10px;width:100px'; +document.body.append(img); + +// show it img.src = URL.createObjectURL(blob); -// show it for 2 seconds -document.body.append(img); -img.style = 'position:fixed;top:10px;left:10px;width:100px'; -setTimeout(() => img.remove(), 2000); +setTimeout(() => { // hide after two seconds + img.remove(); + URL.revokeObjectURL(img.src); +}, 2000); ``` -## Fetch API in detail +```warn +Please note: we can use only one of these methods. -The second argument provides a lot of flexibility to `fetch` syntax. - -Here's the full list of possible options with default values (alternatives commented out): - -```js -let promise = fetch(url, { - method: "GET", // POST, PUT, DELETE, etc. - headers: { - "Content-Type": "text/plain;charset=UTF-8" - }, - body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams - referrer: "about:client", // "" for no-referrer, or an url from the current origin - referrerPolicy: "", // no-referrer, no-referrer-when-downgrade, same-origin... - mode: "cors", // same-origin, no-cors, navigate, or websocket - credentials: "same-origin", // omit, include - cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached - redirect: "follow", // manual, error - integrity: "", // a hash, like "sha256-abcdef1234567890" - keepalive: false, // true - signal: undefined, // AbortController to abort request - window: window // null -}) -``` - - - - - -## How to track progress? - -To track download progress, we need to use `response.body`. - -It's a "readable stream" - a special object that provides access chunk-by-chunk. - -Here's the code to do this: - -```js -const reader = response.body.getReader(); - -while(true) { - // done is true for the last chunk - // value is Uint8Array of bytes - const chunk = await reader.read(); - - if (chunk.done) { - break; - } - - console.log(`Received ${chunk.value.length} bytes`) -} -``` - -We do the infinite loop, while `await reader.read()` returns response chunks. - -A chunk has two properties: -- **`done`** -- true when the reading is complete. -- **`value`** -- a typed array of bytes: `Uint8Array`. - -The full code to get response and log the progress: - -```js run async -// Step 1: start the request and obtain a reader -let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits?per_page=100'); - -const reader = response.body.getReader(); - -// Step 2: get total length -const contentLength = +response.headers.get('Content-Length'); - -// Step 3: read the data -let receivedLength = 0; -let chunks = []; -while(true) { - const {done, value} = await reader.read(); - - if (done) { - break; - } - - chunks.push(value); - receivedLength += value.length; - - console.log(`Received ${receivedLength} of ${contentLength}`) -} - -// Step 4: join chunks into result -let chunksAll = new Uint8Array(receivedLength); // (4.1) -let position = 0; -for(let chunk of chunks) { - chunksAll.set(chunk, position); // (4.2) - position += chunk.length; -} - -// Step 5: decode -let result = new TextDecoder("utf-8").decode(chunksMerged); -let commits = JSON.parse(result); - -// We're done! -alert(commits[0].author.login); -``` - -Let's explain that step-by-step: - -1. We perform `fetch` as usual, but instead of calling `response.json()`, we obtain a stream reader `response.body.getReader()`. - - Please note, we can't use both these methods to read the same response. Either use a reader or a response method to get the result. -2. Prior to reading, we can figure out the full response length by its `Content-Length` header. - - It may be absent for cross-domain requests (as in the example) and, well, technically a server doesn't have to set it. But usually it's at place. -3. Now `await reader.read()` until it's done. - - We gather the `chunks` in the array. That's important, because after the response is consumed, we won't be able to "re-read" it using `response.json()` or another way (you can try, there'll be an error). -4. At the end, we have `chunks` -- an array of `Uint8Array` byte chunks. We need to join them into a single result. Unfortunately, there's no single method that concatenates those. - 1. We create `new Uint8Array(receivedLength)` -- a same-type array with the combined length. - 2. Then use `.set(chunk, position)` method that copies each `chunk` at the given `position` (one by one) in the resulting array. -5. We have the result in `chunksAll`. It's a byte array though, not a string. - - To create a string, we need to interpret these bytes. The built-in `TextEncoder` does exactly that. Then we can `JSON.parse` it. - - What if it were a binary file? We could make a blob of it: - ```js - let blob = new Blob([chunksAll.buffer]); - ``` - -```js run async -let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits?per_page=100'); - -const contentLength = +response.headers.get('Content-Length'); - -const reader = response.body.getReader(); - -let receivedLength = 0; -let chunks = []; -while(true) { - const chunk = await reader.read(); - - if (chunk.done) { - console.log("done!"); - break; - } - - chunks.push(chunk.value); - - receivedLength += chunk.value.length; - console.log(`${receivedLength}/${contentLength} received`) -} - -let chunksMerged = new Uint8Array(receivedLength); -let length = 0; -for(let chunk of chunks) { - chunksMerged.set(chunk, length); - length += chunk.length; -} - -let result = new TextDecoder("utf-8").decode(chunksMerged); -console.log(JSON.parse(result)); +If we get `response.text()`, then `response.json()` won't work, as the body content has already been processed. ``` diff --git a/7-network/10-xmlhttprequest/article.md b/7-network/10-xmlhttprequest/article.md index cac3dc53..e419c2e0 100644 --- a/7-network/10-xmlhttprequest/article.md +++ b/7-network/10-xmlhttprequest/article.md @@ -378,7 +378,7 @@ xhr.send(json); The `.send(body)` method is pretty omnivore. It can send almost everything, including Blob and BufferSource objects. -## Tracking upload progress +## Upload progress The `progress` event only works on the downloading stage. @@ -398,7 +398,7 @@ Here's the list: - `timeout` -- upload timed out (if `timeout` property is set). - `loadend` -- upload finished with either success or error. -Here's an example of upload tracking: +Example of handlers: ```js xhr.upload.onprogress = function(event) { @@ -414,6 +414,36 @@ xhr.upload.onerror = function() { }; ``` +Here's a real-life example: file upload with progress indication: + +```html run + + + +``` ## Summary diff --git a/7-network/10-xmlhttprequest/post.view/server.js b/7-network/10-xmlhttprequest/post.view/server.js index 5e182a2b..50cfc5ae 100644 --- a/7-network/10-xmlhttprequest/post.view/server.js +++ b/7-network/10-xmlhttprequest/post.view/server.js @@ -17,9 +17,9 @@ function accept(req, res) { chunks.push(data); length += data.length; - // Too much POST data, kill the connection! - if (length > 1e6) { - request.connection.destroy(); + // More than 10mb, kill the connection! + if (length > 1e8) { + req.connection.destroy(); } }); @@ -32,6 +32,9 @@ function accept(req, res) { } else if (req.url == '/image') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: "Image saved", imageSize: length })); + } else if (req.url == '/upload') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ message: "Upload complete", size: length })); } else { res.writeHead(404); res.end("Not found"); diff --git a/7-network/2-fetch-crossorigin/article.md b/7-network/2-fetch-crossorigin/article.md index 54676d78..8c7976f2 100644 --- a/7-network/2-fetch-crossorigin/article.md +++ b/7-network/2-fetch-crossorigin/article.md @@ -1,27 +1,16 @@ # Cross-Origin Fetch -TODO: -Note that "Content-Length" header is not returned by default for CORS requests - -== - -Cache-Control -Content-Language -Content-Type -Expires -Last-Modified -Pragma - - If we make a `fetch` from an arbitrary web-site, that will probably fail. -Fetching from another origin (domain/port/protocol triplet) requires special headers from the remote side. +The core concept here is *origin* -- a domain/port/protocol triplet. -For instance, let's try fetching from http://google.com: +Cross-origin requests -- those sent to another domain or protocol or port -- require special headers from the remote side. + +For instance, let's try fetching from `http://example.com`: ```js run async try { - await fetch('http://google.com'); + await fetch('http://example.com'); } catch(err) { alert(err); // Failed to fetch } @@ -29,27 +18,55 @@ try { Fetch fails, as expected. -## Safety control +## Why? -Cross-origin requests pass a special safety control, with the sole purpose to protect the internet from evil hackers. +Cross-origin requests are subject to the special safety control with the sole purpose to protect the internet from evil hackers. -Seriously. For many years cross-domain requests were simply unavailable. The internet got used to it, people got used to it. +Seriously. Let's make a very brief historical digression. -Imagine for a second that a new standard appeared, that allows any webpage to make any requests anywhere. +For many years Javascript was unable to perform network requests. + +The main way to send a request to another site was an HTML `
` with either `POST` or `GET` method. People submitted it to ` +``` + +So, it *was* possible to make a request. But if the submission was to another site, then the main window was forbidden to access `