Async programming & network request in JavaScript

async actions: init now but finish later (code below it won’t wait) ..

Callback

loadScript('xxx.js', (error, script) => { .. }) (“error-1st” cb style) ..: cb hell / pyramid of doom
Fn Call Scheduling: setTimeout/setInterval(..) .. (enqueue in macrotask ..)

1
2
let timerId = setTimeout/setInterval(fn, delay=0, ...args); // keep fn-ref in scheduler (not GC) 
clearTimeout/clearInterval(timerId); // "timerId keep same" af clear (not null)
  • setTimeout(): set this=window ..
  • setInterval(): fn-exe-time > delay?
  • nested setTimeout vs setInterval ..
  • tasks: Output every second

Promise

new Promise((resolve, reject) => .. ).then(value => .. ).catch(err => .. ).finally(fn) ..

  • error occur in setTimeout (except reject(err)) not catched ..
  • unhandlerd rejection & window unhandledrejection event ..
  • cb vs promise: promise only single result (cb can be called mtp times) ..

async/await ..

  • async bf fn: return a promise & enable await
  • await & try-catch — promise.then() & catch()

Promise API

  • let promise = Promise.all/allSettled/race(...promises); ..
    • Promise.all(): all resolved or reject
    • Promise.allSettled(): [...{ status: 'fulfilled/rejected', value/reason: xxx }]
    • Promise.race(): 1st settled promise.result
  • Promise.resolve(value)/reject(err): a resolved / rejected promise, same as
    new Promise((resolve, reject) => resolve(value) || reject(error))

Abort async task: AbortController

  • when call ctrller.abort() called ..
    • ctrller.signal emit abort event (ctrller.signal.aborted = true)
    • ctrller.signal.addEL('abort', handler)
  • abort fetch op: fetch(url, { signal: ctrller.signal }) ..
    • when ctrller.abort() called, fetch get event from signal then aborted & promise rejected with AbortError

Network request

AJAX: load new info without page reload

  • network method .., ..: fetcch
  • built-in obj to make HTTP requests: XHR ..

fetch & XHR

network method fetch(url, [request-options]) ..fetch API

  • fetch(url): GET request
  • request-options: { method, headers: { 'Content-Type' .. }, body } .., ..

2-stage response

  • await fetch(..): response: server response with headers (returned promise resolve with obj of Response) ..
    • response.status, response.ok (if HTTP-status 200-299 — 404 / 500 do not cause promise reject)
    • response.headers: map-like obj ..
  • get response body: response.json()/blob() .. (asyn)

progress track

  • download: response.body.getReader().read(): {done, value} & +response.headers.get('Content-Length') ..
    • response.body: ReadableStream, repeatedly call read() to read body chunk-by-chunk & concat value.length
  • upload: xhr.upload.onprocess & e.loaded / e.total ..
    • xhr.upload.onprocess: trigger when data sent ..resume file upload algorithm
    • xhr.onprogress: trigger when download response .. (a data packet of the response arrived ..)

Task: getGithubUsers(names)

CORS

  • origin: “protocol://domain:port” .., .. & same origin policy: only allow to set location .. & get ref of iframe.contentWindow ..
    • make same 2nd-level domain wins same origin: document.domain = 'site.com' ..
  • cross-win msg (from any origin) — win postMessage(..) interface ..
    • sender-win call receiverWin.postMessage(..) — trigger receiverWin.message event in receiver-win
    • note: add handler to receiverWin.message event by addEventHandler() ..
  • the old ways to work around cors: <form> & <script>
    • <form target="iframe" ..>: open action.url in ifame
    • script & JSONP protocol: <script src="another-site.com?callback=xxx">
      • remote dynamically generates a script, load and exe & call our local fn xxx(data)

Fetch & CORS

Cross-origin requests & special response headers

  • simple request: browser send header OriginAccess-Control-Allow-Origin
  • non-simple request: browser extra “preflight” request (ask for permission 1st)
    • method OPTIONS + A-C-Request-Method / *-HeadersA-C-A-Origin/*-Methods / *-Headers
  • js default accessible response headers — Access-Control-Expose-Headers
  • request with credentials , ..
  • request header: Origin vs Referer — fetch referrer, referrerPolicy option
  • http code 301, 302 & fetch option redirect
  • window.onunload & fetch option keepalive

B/S communication: Polling & WebSocket

Get msg from server

  • Regular polling: msg pass delay, server bombed with requests
  • Long polling: request (e.g. fetch) → server keep conn open until response with data → response process & re-request
    • good when msgs rare ..
    • timeout status 502
  • Server Sent Events EventSource — vs WebSocket: ES support auto-reconn
    • new EventSource(url, ..): start conn ..open (connected) / error event (response wrong Content-Type / HTTP status (e.g. 500 ..) ..)
    • server send data (response 200 & Content-Type: text/.. to write text msg) ..
      • message event: es.onmessage(e.data)
      • custom event: es.addEL(..) (data come with event field) ..
    • browser close conn: es.close() ..
    • no reconn: response 204 | error event ..
      • check if reconn when error: es.onerror es.readyState === EventSource.CONNECTING ..
    • response msg fields: retry (for timeout), event (for custom events), data, id (for reconn) …

Data exchange: WebSocket: persistent conn, no cors limitation ..

  • new WebSocket("wss://xxx ", ..): start conn ..open event: connected
    • wss:// protocol (WebSocket over TLS) ..
  • data exchange: ws.send(data) & ws.onmessage(e.data) ..
    • data type: only string/bin in browser (“frame”) ..
    • ws.send(data) onopen — ws.bufferedAmount: # of bytes to be sent (when network slow) ..
  • conn close: ws.close(code=1000, reason); & ws.onclose:e.code/reason/wasClean

Storing data in the browser

Cookie: name=value; pairs stored in browser ..

  • part of HTTP protocol ..
    • request to server & server set a corresponding cookie using response Set-Cookie HTTP-header
    • subsequent request to the same domain, browser auto add them (in Cookie HTTP-header) , e.g. in auth
  • read/write by document.cookie .., ..
    • 4kb/cookie, 20+/site (for network transfer limitation) ..
    • unaccessible to js if httpOnly option set ..
  • cookie options …
    • by default, cookie rmed af browser close (session cookie) — expires / max-age ..
    • secure: only accessible over HTTPS (if set in https://) ..
    • samesite: prevent XSRF attack — not sent cookie if request comes from outside ..
      • only work for browser af 2017, ignored by old browsers — use with the old solution
      • XSRF attack: user submit a form to bank.com in evil.com & browser auto-sent auth-cookie
      • old solution: forms generated by bank.com have a special “XSRF protection token” & when receive form, bank.com checks for such token
  • 3rd-party cookies: set by domain other than the page visiting ..
    • the cookie bound to the domain, can use to track user when moves btn sites
    • GDPR enforce explicit user permission to set it

Web storage obj: local/sessionStorage

save key/value pairs (str-only ..) in browser by web storage obj ..

  • localStorage: data shared btn all wins with the same origin & not expire ..
  • sessionStorage: only current browser tab (even page refresh, but not re-open) ..
  • browser storage obj vs cookie: not sent to server, so larger storage (up to 5mb ..) ..
  • window.onstorage event: triggers when data updated in local/sessionStorage
  • triggers on all win-obj where the storage accessible, except the causing one — allow same origin wins to exchange msgs
  • task: Autosave a form field

indexedDB

indexedDB: mtp types of key/value, bigger storage (intended for offline apps), support transaction

  • let openRequest = indexedDB.open(DBNname: str, schemaVersionNo: int);: conn to a DB, bound to current origin
  • openRequest obj events:
    • error & console.error("Error", openRequest.error)
    • success & let db = openRequest.result
    • upgradeneeded: when client local db version e.oldVersion < arg schemaVersionNo e.newVersion
      • e.g. user come 1st time (db not exist yet, version = 0),
      • or client load old code (prevent by HTTP caching headers, or prompt reload page)
      • handler finishes without errs => trigger openRequest.onsuccess
  • Parallel update problem: both old/new db conn exists (in diff tabs)
    • versionchange event trigger in tab1 (old version db conn): close old db conn & reload page (see db.onversionchange)
    • if old db conn not close, open() in tab2 trigger block event (ask tab2 user to close other tabs for update)
    • variants ..
  • delete db by let deleteRequest = indexedDB.deleteDatabase(name)
    • check deleteRequest.onsuccess/onerror for deletion result
  • object store (i.e. table in RDB)
    • db.create/deleteObjectStore(..), objectStoreNames.contains(..) ..
    • created/removed/altered obj-store only in upgradeneeded handler (auto-generate a versionchange transaction)
    • can make data-ops outside upgradeneeded handler
    • obj-store CRUD requests
      • search by key: IDBKeyRange obj & store.get*([query]) (values sorted by key order) ..
        by other fields by index (return a obj-store-liked obj keyed by that field, could mtp-value) ..
      • add/update & delete data request: store.add/put(obj), store.delete(..)/clear()
    • iter obj-store by cursor ..
  • transaction
    • starts t & get it obj-store: let t = db.transaction(..) let books = t.objectStore("books")
      & other request cmds
    • request.onsuccess/error & request.result/error
    • all t.requests finish => microtasks queue empty => auto-commit => t.oncomplete event
      • async ops/macrotask not allowed btn t.requests: t closed bf macrotasks starts => TransactionInactiveError
      • solution: split t ops & macrotasks/async ops apart ..
    • t.abort() & t.onabort event, t.error
      • failed request auto abort t, can be prevent in request.onerror handler by e.preventDefault()
  • IndexedDB events bubble: requesttransactiondatabase
    • db.onerror = function(event) { let request = event.target.. }
  • Promise wrapper & try..catch vs Adding onsuccess/onerror to every request
  • demo in summary ..