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 | let timerId = setTimeout/setInterval(fn, delay=0, ...args); // keep fn-ref in scheduler (not GC) |
- 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
unhandledrejectionevent .. - cb vs promise: promise only single result (cb can be called mtp times) ..
async/await ..
asyncbf fn: return a promise & enableawaitawait&try-catch— promise.then() & catch()
Promise API
let promise = Promise.all/allSettled/race(...promises);..Promise.all(): all resolved or rejectPromise.allSettled():[...{ status: 'fulfilled/rejected', value/reason: xxx }]Promise.race(): 1st settled promise.result
Promise.resolve(value)/reject(err): a resolved / rejected promise, same asnew Promise((resolve, reject) => resolve(value) || reject(error))
Abort async task: AbortController
- when call
ctrller.abort()called ..ctrller.signalemitabortevent (ctrller.signal.aborted= true)ctrller.signal.addEL('abort', handler)
- abort fetch op:
fetch(url, { signal: ctrller.signal })..- when
ctrller.abort()called, fetch get event fromsignalthen aborted & promise rejected withAbortError
- when
Network request
AJAX: load new info without page reload
fetch & XHR
network method fetch(url, [request-options]) .. — fetch API
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 & concatvalue.length
- upload:
xhr.upload.onprocess&e.loaded / e.total..xhr.upload.onprocess: trigger when data sent .. — resume file upload algorithmxhr.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 ofiframe.contentWindow..- make same 2nd-level domain wins same origin:
document.domain = 'site.com'..
- make same 2nd-level domain wins same origin:
- cross-win msg (from any origin) — win
postMessage(..)interface ..- sender-win call
receiverWin.postMessage(..)— triggerreceiverWin.messageevent in receiver-win - note: add handler to
receiverWin.messageevent byaddEventHandler()..
- sender-win call
- 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)
- remote dynamically generates a script, load and exe & call our local fn
Fetch & CORS
Cross-origin requests & special response headers …
- simple request: browser send header
Origin—Access-Control-Allow-Origin… - non-simple request: browser extra “preflight” request (ask for permission 1st)
- method
OPTIONS+A-C-Request-Method/*-Headers—A-C-A-Origin/*-Methods/*-Headers…
- method
- js default accessible response headers —
Access-Control-Expose-Headers… - request with credentials …, ..
- request header:
OriginvsReferer… — fetchreferrer, referrerPolicyoption … - 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— vsWebSocket: ES support auto-reconnnew EventSource(url, ..): start conn .. —open(connected) /errorevent (response wrongContent-Type/ HTTP status (e.g. 500 ..) ..)- server send data (response 200 &
Content-Type: text/..to write text msg) ..messageevent:es.onmessage(e.data)- custom event:
es.addEL(..)(data come witheventfield) ..
- browser close conn:
es.close().. - no reconn: response 204 |
errorevent ..- check if reconn when error:
es.onerrores.readyState === EventSource.CONNECTING..
- check if reconn when error:
- 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 .. —openevent: connectedwss://protocol (WebSocket over TLS) ..
- data exchange:
ws.send(data)&ws.onmessage(e.data).. - conn close:
ws.close(code=1000, reason);&ws.onclose:e.code/reason/wasClean…
Storing data in the browser
Cookie
Cookie: name=value; pairs stored in browser ..
- part of HTTP protocol ..
- request to server & server set a corresponding cookie using response
Set-CookieHTTP-header - subsequent request to the same domain, browser auto add them (in
CookieHTTP-header) , e.g. in auth
- request to server & server set a corresponding cookie using response
- read/write by
document.cookie.., .. - cookie options …
- by default, cookie rmed af browser close (session cookie) —
expires/max-age.. secure: only accessible over HTTPS (if set inhttps://) ..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.cominevil.com& browser auto-sent auth-cookie - old solution: forms generated by
bank.comhave a special “XSRF protection token” & when receive form,bank.comchecks for such token
- by default, cookie rmed af browser close (session cookie) —
- 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.onstorageevent: triggers when data updated inlocal/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.resultupgradeneeded: when client local db versione.oldVersion< argschemaVersionNoe.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)
versionchangeevent trigger in tab1 (old version db conn): close old db conn & reload page (seedb.onversionchange)- if old db conn not close,
open()in tab2 triggerblockevent (ask tab2 user to close other tabs for update) - variants ..
- delete db by
let deleteRequest = indexedDB.deleteDatabase(name)- check
deleteRequest.onsuccess/onerrorfor deletion result
- check
- object store (i.e. table in RDB)
db.create/deleteObjectStore(..), objectStoreNames.contains(..)..- created/removed/altered obj-store only in
upgradeneededhandler (auto-generate aversionchangetransaction) - can make data-ops outside
upgradeneededhandler - obj-store CRUD requests
- search by key:
IDBKeyRangeobj &store.get*([query])(values sorted by key order) ..
by other fields byindex(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()
- search by key:
- 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.oncompleteevent- async ops/macrotask not allowed btn t.requests: t closed bf macrotasks starts =>
TransactionInactiveError - solution: split t ops & macrotasks/async ops apart ..
- async ops/macrotask not allowed btn t.requests: t closed bf macrotasks starts =>
t.abort()&t.onabortevent,t.error- failed request auto abort t, can be prevent in
request.onerrorhandler bye.preventDefault()
- failed request auto abort t, can be prevent in
- starts t & get it obj-store:
- IndexedDB events bubble:
request→transaction→databasedb.onerror = function(event) { let request = event.target.. }
- Promise wrapper &
try..catchvs Addingonsuccess/onerrorto every request - demo in summary ..