PWA

1.1 Promises & Fetch

Module Still Under Development

# Introductory Topics

These are the topics that we will be discussing in the first class.

  • Course Topics
  • Workload
  • Assignments and Deliverables
  • Hybrid portion of course
  • The connection between MAD9124 and MAD9022
  • Slack channel and workspace
  • Plagiarism
  • Chatgpt and code pilot
  • Promises
  • Fetch, Http Request and Response, Files, and URLs

# Promises

A Promise is a primary structure to be used with asynchronous programming.

You already have good references for Promise and asynchronous programming in the MAD9014 notes. Module 9.1 (opens new window) and Module 9.2 (opens new window) explain Promises, async - await, and the event loop.

When you create an event listener, the function is put aside in a holding area waiting to be told that the event has occurred. When the event happens, the function is moved into the task queue. The next time around the event loop, the next task will get run. setInterval and setTimeout also use the task queue for their functions.

Promises get added to another queue called the microtask queue. MutationObservers also use the microtask queue. The main differences between the task and microtask queue are the order that they are called from the event loop, and that the microtask wants to empty all of its tasks before continuing, whereas the task queue runs one of its tasks per time through the event loop.

This video gives a great explanation of the Event Loop.

# Async Await

The async and await keywords are a way of converting an ordinary function into one that can be paused to wait for the completion of one or more promises.

Here is an example of a typical function. The log calls will happen in the order of the numbers - one, two , three.

function basic(url) {
  console.log('one');
  fetch(url).then(() => {
    console.log('three');
  });
  console.log('two');
}
1
2
3
4
5
6
7

With async and await you can tell the function that you might want to pause and wait for an asynchronous (Promise) call to be finished before proceeding.

async function basic(url) {
  console.log('one');
  await fetch(url);
  console.log('three');
  console.log('two');
}
1
2
3
4
5
6

This version of the function would log the numbers as - one, three, two.

# Functions and Promises

When it comes to synchronous code things happen in a logical sequence. The first line of code runs first. Function calls run and complete before moving to the next line of code. In the following example, the three variables result1, result2, result3 will all be assigned a return value from the associated functions.





 


function dosomething(){
  let result1 = firstfunc();
  let result2 = secondfunc();
  let result3 = thirdfunc();
  console.log(result1, result2, result3);
}
1
2
3
4
5
6

This works and it is easy to see how. However, if any of those three functions are asynchronous then this will not work. The final line will run before the result is back from any asynchronous function.

If you ever need to test and check if a function is asynchronous, you can check to see what constructor was used to create the function. If a function is created with a signature like async function x(){ } then you can use x.constructor.name === 'AsyncFunction' to test it.

async function dosomething() {
  let result1, result2, result3;
  if (firstfunc.constructor.name === 'AsyncFunction') {
    result1 = await firstfunc();
  } else {
    result1 = firstfunc();
  }
  if (secondfunc.constructor.name === 'AsyncFunction') {
    result2 = await secondfunc();
  } else {
    result2 = secondfunc();
  }
  if (thirdfunc.constructor.name === 'AsyncFunction') {
    result3 = await thirdfunc();
  } else {
    result3 = thirdfunc();
  }
  console.log(result1, result2, result3);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Technically, for this example you could just put await in front of each function call, regardless of whether or not it is an asynchronous one, but this won't always be the case. There may be times when you need to know whether or not a function is asynchronous or not.

Now, if you know that the function that you are calling is going to return a Promise, then you can write your code to address that.

Say you have several modules that you are importing to use and that they have a series of methods which will return a Promise.

import { getPeopleFromDatabase } from 'db.js';
import { getImagesFromCache } from 'cache.js';
import { getInfoFromAPI } from 'api.js';
1
2
3

Each of those methods will return a Promise. A Promise will either resolve or reject. It will either work or fail with an error. That means we have to wrap everything in a try{ }catch(err){ } OR we can chain the calls together in a series of then() methods with a single catch() at the end.

Just decide on the order that you need these methods to run.

function doSomething() {
  getPeopleFromDatabase()
    .then((resultFromDB) => {
      //use the results from the DB
      return getInfoFromAPI();
    })
    .then((resultFromAPI) => {
      //use the results from the API
      return getImagesFromCache();
    })
    .then((resultFromCache) => {
      //use the results from the cache
    })
    .catch((err) => {
      //handle the error from one of the promises
      switch (err.name) {
        case 'DBError':
          //handle an error from the DB
          break;
        case 'APIError':
          //special error from the API call
          break;
        case 'CacheError':
          //special error from the Cache call
          break;
        case 'Error':
        //a general JavaScript error
      }
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

The catch method in the code above is looking at the name property of the error object. JavaScript has a series of built-in error types - EvalError, InternalError, RangeError, ReferenceError, SyntaxError, TypeError, and URIError. These are all built on top of the base Error object.

If you want, you can create your OWN error types to use in your code.

The JavaScript Class syntax is an easy way to do this. Here is an example of how to create your own custom error.

class HungryError extends Error {
  thirstyToo;
  message = '';

  constructor(message, thirstyToo = false) {
    super('HungryError'); //the name property
    this.message = message;
    this.thirstyToo = thirstyToo;
  }
}
export { HungryError };
1
2
3
4
5
6
7
8
9
10
11

This HungryError has the name property which gets saved via the Error class. Then it saves the message in an instance variable belonging to the HungryError object that you create. The thing that differentiates HungryError from the basic Error is the thirstyToo property.

If you want to use this error in a file that you create...

import { HungryError } from 'hungry-error.js';

function dosomething() {
  try {
    //do some things
    //pretend that it fails
    throw new HungryError('Too hungry to code', true);
  } catch (err) {
    console.log(err.name); //HungryError
    console.log(err.message); //Too hungry to code
    console.log(err.thirstyToo); //true
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Promise Methods

Any time that you want to deal with multiple promises at the same time any you don't want to take the next step in your code before ALL the promises have resolved, then you can use one of these methods - all, race, allSettled, and any.

  • all waits for all of the Promises to be resolved or rejects when the first one is rejected.
  • allSettled waits for all the Promises to be resolved or rejected. Provides array of all results and errors. Rejects if ALL the promises are rejected.
  • race waits for the first response (resolve or reject) to call then or catch.
  • any waits for the first successful resolve. Only rejects if all the promises fail.

For all and allSettled you will have an array of results passed to the .then(results=>{}).

For race and any you will get the one result, just like calling a single Promise.

When you get an array of results from all or allSettled, each of the values in the array is an object with two main properties - status and value. Inside the then method, all the status values will be fulfilled. The value properties will be the actual object returned by the promise. For example, with a call to fetch(), the value will be a Response object.

Remember to view the MAD9014 promise content (opens new window) for more information.

# Fetch

This video covers everything that you need to know about using the Fetch API.

Watch this video this week. Refer back to it as a reference when working with Fetch, Request objects, Response objects, and URL objects.

# Requests and Responses

As files are passed back and forth between the browser and the server, they need to be wrapped inside a Request or Response object. Each of these objects have a body property which holds the file.

The Request object gets a String value, or FormData object, or a File object assigned to the body property.

The Response object needs to use a method to extract the file from the body property. The most common methods are text(), json(), or blob(), which are used for HTML/XML, JSON, or media files.

The Response object also has an ok property which is a Boolean indicating success receiving a response with a status code between 200 and 299. The status and statusText properties tell you the exact status of the response.

If you want details about the contents of the response your best option is the headers property, which has a get method. It can be passed the name of any header and it will return the value of that header.

# JSON vs JS

JSON and JavaScript are both text file types - application/json and text/javascript.

The difference is that the first is a data format (just data) and the latter is a file format that is read and interpreted by a JavaScript engine such as V8.

When writing objects in JS, the keys are only allowed to be strings. The quotation marks can be omitted for the keys. The quotation marks for the values can be single, double, or backticks. JavaScript allows for trailing commas, comments, and expects variables to be declared to hold the values.

//this is a comment about some data
//this is an array of objects
const data = [
  { id: 1, name: 'Sam', email: 'sam@work.org' },
  { id: 2, name: 'Dean', email: 'dean@work.org' },
  { id: 3, name: 'Cas', email: 'cas@work.org' },
];
1
2
3
4
5
6
7

The JSON version of the same information would look very similar. All the quotation marks need to be double quotes. No comments are allowed. No trailing commas are allowed.

[
  { "id": 1, "name": "Sam", "email": "sam@work.org" },
  { "id": 2, "name": "Dean", "email": "dean@work.org" },
  { "id": 3, "name": "Cas", "email": "cas@work.org" }
]
1
2
3
4
5

JSON is just one type of data file. Text data can be saved as json, txt, xml, csv, html, tsv, or other formats. The purpose of any of these formats is just to be able to transfer information between different computers or between different computers.

Web Services largely work by sending information back and forth as these files.

Some Web Services also use web sockets to transfer data as a stream. But that's a topic for another day.

# URL objects

The fetch method can accept three different types of initial arguments.

  1. A String which is a url.
  2. A URL object built from a string.
  3. A Request object that contains a URL object.

If you pass a String to the fetch method, it will wrap that in a URL which will in turn be wrapped in a Request object.

The Request object has a url property that contains the URL object.

The benefit of using a Request instead of a String is that the Request has already divided the string url into a series of properties that represent each of the parts of the url, like protocol, pathname, host, etc.

The other benefit that you get with a URL object is the ability to create a URL that points to some data in memory. Let's say that you do a fetch for an image. The Response object back from the server will contain the binary data for the image.

let blobData = await myImageResponseObject.blob();
let url = URL.createObjectURL(blobData);
let img = document.createElement('img');
img.src = url;
1
2
3
4

The binary data can then be passed to the URL.createObjectURL() method which will generate a URL that points to the memory location with that binary data. The URL can then be used by elements like <img> to load the image data.

# Files

File objects represent files from your operating system as well as files that are in a Request or Response object.

A user can pass a file to the browser through an <input type="file" /> element. There is also a new system Origin Private File System (OPFS) designed to allow for access to the device file system. However, it does NOT have full support across all browsers. So, this leaves us with the input element approach.

The input element has an accept attribute that allows you to filter the types of files that you are willing to accept. If added, the multiple attribute lets the user select more than one file at a time.

<form id="myForm">
  <input type="file" id="avatarID" name="avatarNM" accept="image/*, audio/*" multiple required />
</form>
1
2
3

In JavaScript, the input element will always have a file property that holds a FileList object. It has this list regardless of how many files the user picks.

let form = document.getElementById('myForm');
//there are different ways to access the elements
form.addEventListener('submit', (ev) => {
  let input;
  input = form.elements.avatarNM;
  input = document.getElementById('avatarID');
  let filelist = input.files;
  //filelist is an array of files.
  if (filelist.length > 0) {
    //at least one file submitted
    let fileOne = filelist[0];
    console.log(fileOne.name);
    console.log(fileOne.type);
    console.log(fileOne.size);
    console.log(fileOne.lastModified);
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

If you want to build your own File object you can do that with the new File(fileBits, fileName, options) constructor. Only the filebits is a required parameter. It needs to be an Iterable object like an Array, TypedArray, or Blob. The filename is what you want to use as the name of the file and the options is usually the mime-type, like application/json or text/xml or image/jpg.

If you want to create a JSON file, you can take a JavaScript object, convert it to a JSON string with JSON.stringify() and then pass that string to the File constructor, wrapped in an Array.

let data = {
  hello: 'world',
};
let str = JSON.stringify(data);
let f = new File([str], 'mydata.json', { type: 'application/json' });
1
2
3
4
5

File object reference (opens new window)

Blob object reference (opens new window)

# ToDo This Week

To Do this week

Last Updated: 1/15/2024, 3:06:04 PM