Node.js

Calvin (Deutschbein)

Week 12

Cloud

Announcements

  • We have now 'learned' the front-end (browser) web technologies.
  • We can now "recycle" that knowledge into back-end (server) web technologies.
  • This lecture - on Node.js - closes the loop on "what the cloud does"
    • vs Spark, which answers "how").

Prep work

  • You may wish to:
    • Install Node.js (link)
    • Clone + Run 'docker-nodejs-sample' from Github
    • I won't assume you have Node.js but WOW is it easier to use.

Big Idea

  • Big Idea: browsers and servers
    • Browsers (clients) [front end]:
      • HTML
      • CSS
      • JavaScript
    • Servers [back end]:
      • Python (Flask/Django)
      • C (NGINX/HTTPD)
      • SQL

Javascript "Creep"

  • Big Idea: browsers and servers
    • Browsers (clients) [front end]:
      • HTML
      • CSS
      • JavaScript
    • Servers [back end]:
      • JavaScript (Node.js)
      • SQL

Today

  • Big Idea: browsers and servers
    • Front end:
      • HTML
      • CSS
      • JavaScript
    • Servers [back end]:
      • JavaScript (Node.js)
      • SQL

This is our HTML+CSS

index.html <!DOCTYPE html> <html lang="en"> <head> <link rel="stylesheet" href="styles.css"> </head> <body> <p>This page is just a demo.</p> </body> </html> styles.css p { background: blue; color: white; }

They just have to be in the same directory.

    $ ls -al *.html *.css -rw-r--r-- 1 cd-desk 197121 161 Jun 20 11:05 index.html -rw-r--r-- 1 cd-desk 197121 47 Jun 20 11:06 styles.css

HTML and CSS

All of HTML/CSS gives static styling.

HTML is a

  • Element
    • Start Tag
      • Attributes
        • Attribute value
        • Attribute name
    • Content
    • End Tag
  • Element
    • Void Tag
      • Attributes
        • ...

CSS is a

  • Rule
    • Selector(s)
    • Declaration Block
      • Declaration
        • Property
        • Value
  • Attribute Value
    • Declaration Block
      • ...

Standalone .js

I see JavaScript "embedded" much more than CSS

  • Most JavaScript is used on a single page, so why factor it out
  • Keeps the scripts and the elements that call/interact together

But there's a lot of .js libraries so we need to practice linking.

    <script type="text/javascript" src="scripts.js">

Something like:

index.html <!DOCTYPE html> <html lang="en"> <head> <link rel="stylesheet" href="styles.css"> <script type="text/javascript" src="scripts.js"></script> </head> <body> <p>Oh I'll need a button won't I?</p> </body> </html> styles.css .dark { /* I like cyan and black */ background-color: black; color: #00ffff; }
scripts.js function toggler() { // using a const to linebreak const bd = document.body; bd.classList.toggle("dark"); }

They just have to be in the same directory.

    $ ls -al *.html *.css *.js -rw-r--r-- 1 cd-desk 197121 337 Jun 20 13:57 index.html -rw-r--r-- 1 cd-desk 197121 97 Jun 20 13:56 scripts.js -rw-r--r-- 1 cd-desk 197121 55 Jun 20 13:55 styles.css

Was: Coding Demo

    Was: As time allows.

    • console.log
    • getElementById
    • .innerHTML

    Or... the end of review...

Running Example: Wordle

    Wordle...

    • Has an HTML webpage that contains content (various letters)
    • Has critical styling (letter/box coloring) via CSS.
    • Updates content without a page reload via JavaScript.
    • Interfaces with a remote server that knows todays word. (Node.js)

    As I see it...

    • index.html
    • styles.css
    • script.js
    • server.js

Index.html

    To begin, let's make an HTML webpage. What will it contain?

    • A head element, containing
      • A head element, containing
        • A title element
        • A style element or a link element to a .css file
        • A script element
    • A body element, containing
      • An element managing input of some kind, with
        • An attribute that invokes a script of
      • An element managing output of some kind, with
        • An id that allows the script to update its content/style

HTML

    <!DOCTYPE html> <html lang="en"> <head> <title>Not Quite Wordle</title> <link rel="stylesheet" href="styles.css"> <script type="text/javascript" src="script.js"></script> </head> <body onload="start()"> <input id='inputElement' type="text" minlength="5" maxlength="5"> <button type="button" onclick="check()">Check</button> <table id="gameboard"></table> </body> </html>

How do you feel about this indentation and these newlines?

Embedded Ex.

  • This part: <input id='inputElement' type="text" minlength="5" maxlength="5"> <button type="button" onclick="check()">Check</button> <table id="gameboard"></table>
  • Looks like this:
  • The 'center' element can make it a bit nicer.
  • If at all possible, mousoever the input field.
  • What is the mouseover text?
  • What about when you type a bit?

Alert

  • By default, check() doesn't do anything.
  • But it can.
  • Let's go to script.js and define a check function. function check() { alert("Ch-Check It Out") ; }
  • I promise that isn't a Beastie Boys reference (it is)
  • Alert is good enough for quick testing purposes... let's try it.
  • Well, that's a start, but is a long way from wordle:
    • We aren't updating page content.
    • We aren't reading the input.

getElementById

  • Oh - that's a JAVASCRIPT OBJECT that REFERS to an HTML ELEMENT it is NOT a string.
  • What does that mean?

ELement.value

  • Oh - that's a VALUE of a PROPERTY of a JAVASCRIPT OBJECT that REFERS to the CONTENT of an HTML ELEMENT and it IS a string.
  • What does that mean?

Element.innerHTML

  • We can use Element.innerHTML to show this content on the page. The Element property innerHTML gets or sets the HTML or XML markup contained within the element.
  • Setting 'innerHTML' is to me, the 2nd of 3 ways to 'print' in .js, the first being alert.
  • Remember the gameboard? <table id="gameboard"></table>
  • Tables are a whole thing in HTML, but for now let's naively just place some plaintext within. function check() { const inEle = document.getElementById('inputElement') ; const gmBrd = document.getElementById('gameboard') gmBrd.innerHTML = inEle.value ; }
  • Try it out...
  • Inspect / view selection source / etc.

Text processing

  • Wordle renders everything in capital letters.
    • 'Capital letters' is not a reference to 'DVNO' (2007) by French electro house duo Justice.
    The toUpperCase() method of String values returns this string converted to uppercase.
  • JavaScript sees heavy usage in text processing (since it's the scripting language for a text mark up language) and has great built-in methods.
  • Emphasis method - we invoke the toUpperCase() call as a 'method' of a string, like so... function check() { const inEle = document.getElementById('inputElement') ; const gmBrd = document.getElementById('gameboard') const inStr = inEle.value ; // lack () means 'property' (data) const upStr = inStr.toUpperCase() ; // have () means 'method' (compute) gmBrd.innerHTML = upStr ; }
  • Try it out...
  • Inspect / view selection source / etc.

console.log()

  • It is comically difficult to use JavaScript while debugging solely with innerHTML and alert
  • The last and greatest print alternative is console.log() The console.log() static method outputs a message to the console.
  • To the... hold on,,, to the WHAT now???
  • There's usually (I can't check Safari) a 'console' tab under the 'inspect' menu in web browsers. function check() { const inEle = document.getElementById('inputElement') ; console.log(inEle) ; const gmBrd = document.getElementById('gameboard') console.log(gmBrd) ; const inStr = inEle.value ; // lack () means 'property' (data) console.log(inStr) ; const upStr = inStr.toUpperCase() ; // have () means 'method' (compute) console.log(upStr) ; gmBrd.innerHTML = upStr ; }
  • Try it out...
  • Check the console!

console.log()

  • I got: <input id="inputElement_pg21" type="text" minlength="5" maxlength="5"> 12.html:522:10 <table id="gameboard_pg21"> 12.html:524:10 asd 12.html:526:10 ASD 12.html:528:10
  • You can also just type in whatever... console.log(6 + 7) ; 13 debugger eval code:1:9 undefined
  • Try it out...

node.js

  • At some point (in 2009) someone (a URochester Math PhD dropout) thought...
    Wait why don't I just make a commandline .js utility instead of having to use the console?"
  • That project became Node.js and JavaScript became the most powerful language in the universe. C:\Users\cd-desk>node Welcome to Node.js v20.12.2. Type ".help" for more information. > console.log(6 + 7) 13 undefined >
  • We can use .js...
    • By installing and invoking 'node' at command line
    • By invoking 'node' inside a node.js docker image
    • Via the 'Node.js' program
    user@DESKTOP-THMS2PJ:~$ node Command 'node' not found, but can be installed with: sudo apt install nodejs

docker

  • If you don't have node but do have docker... C:\Users\cd-desk>docker run -it node:alpine node Unable to find image 'node:alpine' locally alpine: Pulling from library/node c6a83fedfae6: Already exists 8d90f41c769e: Already exists c4f54159f74a: Already exists 6ecb2bd0d8e8: Already exists Digest: sha256:39005f06b2fae765764d6fdf20ad1c4d0890f5ad3e1f39b56a18768334b8ecd6 Status: Downloaded newer image for node:alpine Welcome to Node.js v22.5.1. Type ".help" for more information. > console.log(6 + 7) 13 undefined >
  • node:alpine uses an ultralight OS & downloads about 10x faster than the baseline 'node'
  • The 2nd 'node' is the command to invoke within the container so we get the '>'

Backend

  • With Node.js we can do something a bit different.
  • Rather than write code that runs in a browser...
    • We can write code that hosts a webpage
    • This webpage will be accessible at some url
    • It will not be invoked via a script element
  • Let's look at a minimal example.
  • I am loosely following this guide from W3 Node.js has a built-in module called HTTP, which allows Node.js to transfer data over the Hyper Text Transfer Protocol (HTTP). To include the HTTP module, use the require() method...
  • Note: We will now be using the command 'node server.js' to run a .js program.
  • Previously we used 'node' with no argument to have an interactive .js interpreter.
  • I will begin showing the example outside of Docker, then show Docker

Backend

  • First we create our file, server.js, with some boilerplate. const http = require('http'); function serverFunc(req, res) { res.write('Hello World!') ; res.end() ; } serverObj = http.createServer(serverFunc) ; serverObj.listen(8080) ;
  • The first line includes the server library for HTTP, the hypertext transfer protocol
    • 'require' is like 'import' in Python or 'library' in R
    • 'require' is Node.js specific and not part of base JavaScript
    • There is an 'import' in JavaScript but we will not discuss it - it's a little to 'software engineering'

Backend

  • First we create our file, server.js, with some boilerplate. const http = require('http'); function serverFunc(req, res) { res.write('Hello World!') ; res.end() ; } serverObj = http.createServer(serverFunc) ; serverObj.listen(8080);
  • The second code block defines a function 'serverFunc'
    • This function defines how to take a request 'req' and return a response 'res'
    • These will be HTTP requests asking for a webpage, interfacing with an API, etc.
    • Usually these will be HTML or JSON formatted responses.
    • This minimal example responds with plain text.
    • res.end() is necessary to denote the response isn't waiting for anything.

Backend

  • First we create our file, server.js, with some boilerplate. const http = require('http'); function serverFunc(req, res) { res.write('Hello World!') ; res.end() ; } serverObj = http.createServer(serverFunc) ; serverObj.listen(8080) ;
  • The third code block calls the createServer method from the HTTP module.
    • This method requires a function that processes requests and responses
      • We use serverFunc()
    • It accepts a variety of possible options to configure the server.
    • We can think of this as configuring what webpages could be served but not beginning to serve webpages.
    • We can think of this as configuring without actually turning on the server.

Backend

  • First we create our file, server.js, with some boilerplate. const http = require('http'); function serverFunc(req, res) { res.write('Hello World!') ; res.end() ; } serverObj = http.createServer(serverFunc) ; serverObj.listen(8080) ;
  • The fourth code block calls the listen method from the server object.
    • This takes a configured server and connects it some port.
      • In general, websites are on port 80
      • While learning, we use other, arbitrary ports.
      • 8080 is an official alternate HTTP port
      • When just coding, I often use 8083 which is not for anything at all.
      • Read more about port numbers on Wikipedia

via node

  • With server.js... const http = require('http'); function serverFunc(req, res) { res.write('Hello World!') ; res.end() ; } serverObj = http.createServer(serverFunc) ; serverObj.listen(8080) ;
  • ...and a local 'node' installation, we can come online via node server.js
  • You can access the hosted page at http://localhost:8080 or http://127.0.0.1:8080/
  • What do you expect to find?
  • What do you find?

via docker

  • With docker, we have a complication.
  • The server will listen on some port, but by default, that port will be local within the docker container.
  • We need to...
    • Create a node docker container
    • Configure a port to connect to servers within that container
    • Copy server.js into the docker container
    • Run 'node server.js' within the container
  • We could follow the official 'docker-nodejs-sample' from Docker Github...
  • Or just do a minimal example here, using 'compose' Compose simplifies the control of your entire application stack, making it easy to manage services, networks, and volumes in a single, comprehensible YAML configuration file. Then, with a single command, you create and start all the services from your configuration file.

via Docker

We need to...

  • Create a node docker container
  • Configure a port to connect
  • Copy server.js into the docker container
  • Run 'node server.js' within the container

We create a Dockerfile FROM node:alpine EXPOSE 8080 COPY server.js . CMD node server.js


  • Read more about a Dockerfile here
  • If you use the example repository, 'docker init' will guide you through configuration.
  • Autogenerated / example files tend to be 'heavyweight'
    • They deal with cases more specific than we need to just learn the ideas
    • If you are using this professionally, then it matters.

via Docker

My Dockerfile FROM node:alpine EXPOSE 8080 COPY server.js . CMD node server.js

'docker init' Dockerfile # syntax=docker/dockerfile:1 # Comments are provided throughout this file to help you get started. # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ # Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 ARG NODE_VERSION=20.12.2 FROM node:${NODE_VERSION}-alpine # Use production node environment by default. ENV NODE_ENV production WORKDIR /usr/src/app # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.npm to speed up subsequent builds. # Leverage a bind mounts to package.json and package-lock.json to avoid having to copy them into # into this layer. RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=cache,target=/root/.npm \ npm ci --omit=dev # Run the application as a non-root user. USER node # Copy the rest of the source files into the image. COPY . . # Expose the port that the application listens on. EXPOSE 8080 # Run the application. CMD node src/index.js


  • I think this is a good example of confusion / complexity in cloud computing.

via docker

  • Let's go over 'docker init' though! PS C:\Users\cd-desk\Documents\dev\node> docker init Welcome to the Docker Init CLI! This utility will walk you through creating the following files with sensible defaults for your project: - .dockerignore - Dockerfile - compose.yaml - README.Docker.md Let's get started! ? What application platform does your project use? Node ? What version of Node do you want to use? 20.12.2 ? Which package manager do you want to use? npm ? What command do you want to use to start the app? node server.js ? What port does your server listen on? 8080 ✔ Created → .dockerignore ✔ Created → Dockerfile ✔ Created → compose.yaml ✔ Created → README.Docker.md → Your Docker files are ready! Review your Docker files and tailor them to your application. Consult README.Docker.md for information about using the generated files. What's next? Start your application by running → docker compose up --build Your application will be available at http://localhost:8080

via docker

  • Let's go over 'docker init' though!
  • First: PS C:\Users\cd-desk\Documents\dev\node> docker init Welcome to the Docker Init CLI! This utility will walk you through creating the following files with sensible defaults for your project: - .dockerignore - Dockerfile - compose.yaml - README.Docker.md
  • 'docker init' will make 4 files...
    • .dockerignore
    • Dockerfile
    • compose.yaml
    • README.Docker.md
  • We don't care about the README or the .dockerignore (probably), and we made our own Dockerfile...
  • But we may still need a compose.yaml. Keep that in mind.

via docker

  • Let's go over 'docker init' though!
  • The following are prompts - I had to answer these questions.
  • You can try too! Let's get started! ? What application platform does your project use? Node ? What version of Node do you want to use? 20.12.2 ? Which package manager do you want to use? npm ? What command do you want to use to start the app? node server.js ? What port does your server listen on? 8080
  • As a rule, especially with a smaller application, it's enough enough to use defaults!
    • Sure, we have to use node (that's the point), but
    • We probably don't care about version
    • We probably don't care about package manager (think pip vs conda)
  • We do have to remember our .js file name
  • We do have to remember the port we listen on.

When making our own Dockerfile, we can leave out the things we don't care about (e.g. node version)

via docker

  • Let's go over 'docker init' though!
  • The following are prompts - I had to answer these questions.
  • You can try too! ✔ Created → .dockerignore ✔ Created → Dockerfile ✔ Created → compose.yaml ✔ Created → README.Docker.md → Your Docker files are ready! Review your Docker files and tailor them to your application. Consult README.Docker.md for information about using the generated files. What's next? Start your application by running → docker compose up --build Your application will be available at http://localhost:8080
  • This tells us some useful things!
    • This tells us the docker command 'docker compose up --build'
    • This tells us the location of the server once built!

via docker

  • We can also consult the readme
  • It reiterates the commands & locations. ### Building and running your application When you're ready, start your application by running: `docker compose up --build`. Your application will be available at http://localhost:8080.
  • But there's more...

via docker

  • We can also consult the readme
  • It points out that this is cloud computing technology. ### Deploying your application to the cloud First, build your image, e.g.: `docker build -t myapp .`. If your cloud uses a different CPU architecture than your development machine (e.g., you are on a Mac M1 and your cloud provider is amd64), you'll want to build the image for that platform, e.g.: `docker build --platform=linux/amd64 -t myapp .`. Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) docs for more detail on building and pushing.
  • Dockerhub recently started rate limiting, whatever that is, a broke my demo pipeline.
  • But in late February I was easily staging these containers onto Azure student tier and hosting webpages.
  • The workaround is a bit of a headache, but be aware this was free and easy as recently as the beginning of the term.

via docker

  • Let's close the loop on 'docker init'
  • We haven't resolved the compose.yaml issue without using init. PS C:\dev\word\server> ls Directory: C:\dev\word\server Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 7/30/2024 11:41 AM 193 Dockerfile -a---- 7/29/2024 3:08 PM 62555 server.js PS C:\Users\cd-desk\Documents\dev\word\server> docker compose up --build no configuration file provided: not found
  • I found the compose documentation quite confusing as it is intended, I believe, for multi-container ecosystems like docker-hadoop.
  • You can see for yourself, here's one of several pages: link

via docker

  • Let's close the loop on 'docker init'
  • It's a simple enough matter to review the generate compose.yaml and see what's in there: services: server: build: context: . environment: NODE_ENV: production ports: - 8080:8080
  • It looks like we need to specify a file path (context: .) and the port usage
  • NODE_ENV probably won't affect us: link
  • We say "NODE_ENV" is a software engineering thing, and remove it, and move on.

via Docker

Dockerfile FROM node:alpine EXPOSE 8080 COPY server.js . CMD node server.js

compose.yaml services: server: build: context: . ports: - 8080:8080


  • A bit of overhead, but it works. cd-desk@DESKTOP ~/dev/word/server$ ls Dockerfile compose.yaml server.js cd-desk@DESKTOP ~/dev/word/server$ docker compose up --build 2>/dev/null [+] Building 0.5s (7/7) FINISHED docker:default

    Snip[+] Running 1/0 ✔ Container server-server-1 Created 0.0s Attaching to server-1

End Docker

  • With a fully operational Docker implementation, we will no longer think about where node is running.

Backend

  • With a server live, we should look at a request.
  • Add a console.log() call to 'print' the request.
  • Think: Where will it be printed?
  • To begin: const http = require('http'); function serverFunc(req, res) { console.log(req) ; // This is new. res.write('Hello World!') ; res.end() ; } serverObj = http.createServer(serverFunc) ; serverObj.listen(8080) ;

You will need to restart the server if you edit server.js for changes to be reflected.

Backend

  • With a server live, we should look at a request.
  • By accessing the server, you generate a request that you can view wherever you console.log()'ed <ref *2> IncomingMessage { _events: { close: undefined, error: undefined, data: undefined, end: undefined, readable: undefined }, _readableState: ReadableState { highWaterMark: 16384, buffer: [], bufferIndex: 0, length: 0, pipes: [], awaitDrainWriters: null, [Symbol(kState)]: 1315084 }, _maxListeners: undefined, socket: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _events: { close: [Array], error: [Function: socketOnError], prefinish: undefined, finish: undefined, drain: [Function: bound socketOnDrain], data: [Function: bound socketOnData], end: [Array], readable: undefined, timeout: [Function: socketOnTimeout], resume: [Function: onSocketResume], pause: [Function: onSocketPause] }, _readableState: ReadableState { highWaterMark: 16384, buffer: [], bufferIndex: 0, length: 0, pipes: [], awaitDrainWriters: null, [Symbol(kState)]: 193997060 }, _writableState: WritableState { highWaterMark: 16384, length: 0, corked: 0, onwrite: [Function: bound onwrite], writelen: 0, bufferedIndex: 0, pendingcb: 0, [Symbol(kState)]: 17564420, [Symbol(kBufferedValue)]: null }, allowHalfOpen: true, _maxListeners: undefined, _eventsCount: 8, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, _server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, parser: HTTPParser { '0': null, '1': [Function: parserOnHeaders], '2': [Function: parserOnHeadersComplete], '3': [Function: parserOnBody], '4': [Function: parserOnMessageComplete], '5': [Function: bound onParserExecute], '6': [Function: bound onParserTimeout], _headers: [], _url: '', socket: [Circular *1], incoming: [Circular *2], outgoing: null, maxHeaderPairs: 2000, _consumed: true, onIncoming: [Function: bound parserOnIncoming], joinDuplicateHeaders: null, [Symbol(resource_symbol)]: [HTTPServerAsyncResource] }, on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Circular *1], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: null, [Symbol(errored)]: null, [Symbol(kHighWaterMark)]: 16384, [Symbol(kRejectNonStandardBodyWrites)]: false, [Symbol(kUniqueHeaders)]: null }, [Symbol(async_id_symbol)]: 5, [Symbol(kHandle)]: TCP { reading: true, onconnection: null, _consumed: true, [Symbol(owner_symbol)]: [Circular *1] }, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(shapeMode)]: true, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: false, rawHeaders: [ 'Host', '127.0.0.1:8080', 'User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0', 'Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8', 'Accept-Language', 'en-US,en;q=0.5', 'Accept-Encoding', 'gzip, deflate, br, zstd', 'DNT', '1', 'Connection', 'keep-alive', 'Upgrade-Insecure-Requests', '1', 'Sec-Fetch-Dest', 'document', 'Sec-Fetch-Mode', 'navigate', 'Sec-Fetch-Site', 'none', 'Sec-Fetch-User', '?1', 'Priority', 'u=0, i' ], rawTrailers: [], joinDuplicateHeaders: null, aborted: false, upgrade: false, url: '/', method: 'GET', statusCode: null, statusMessage: null, client: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _events: { close: [Array], error: [Function: socketOnError], prefinish: undefined, finish: undefined, drain: [Function: bound socketOnDrain], data: [Function: bound socketOnData], end: [Array], readable: undefined, timeout: [Function: socketOnTimeout], resume: [Function: onSocketResume], pause: [Function: onSocketPause] }, _readableState: ReadableState { highWaterMark: 16384, buffer: [], bufferIndex: 0, length: 0, pipes: [], awaitDrainWriters: null, [Symbol(kState)]: 193997060 }, _writableState: WritableState { highWaterMark: 16384, length: 0, corked: 0, onwrite: [Function: bound onwrite], writelen: 0, bufferedIndex: 0, pendingcb: 0, [Symbol(kState)]: 17564420, [Symbol(kBufferedValue)]: null }, allowHalfOpen: true, _maxListeners: undefined, _eventsCount: 8, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, _server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, parser: HTTPParser { '0': null, '1': [Function: parserOnHeaders], '2': [Function: parserOnHeadersComplete], '3': [Function: parserOnBody], '4': [Function: parserOnMessageComplete], '5': [Function: bound onParserExecute], '6': [Function: bound onParserTimeout], _headers: [], _url: '', socket: [Circular *1], incoming: [Circular *2], outgoing: null, maxHeaderPairs: 2000, _consumed: true, onIncoming: [Function: bound parserOnIncoming], joinDuplicateHeaders: null, [Symbol(resource_symbol)]: [HTTPServerAsyncResource] }, on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Circular *1], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: null, [Symbol(errored)]: null, [Symbol(kHighWaterMark)]: 16384, [Symbol(kRejectNonStandardBodyWrites)]: false, [Symbol(kUniqueHeaders)]: null }, [Symbol(async_id_symbol)]: 5, [Symbol(kHandle)]: TCP { reading: true, onconnection: null, _consumed: true, [Symbol(owner_symbol)]: [Circular *1] }, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(shapeMode)]: true, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _consuming: false, _dumped: false, [Symbol(shapeMode)]: true, [Symbol(kCapture)]: false, [Symbol(kHeaders)]: { host: '127.0.0.1:8080', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8', 'accept-language': 'en-US,en;q=0.5', 'accept-encoding': 'gzip, deflate, br, zstd', dnt: '1', connection: 'keep-alive', 'upgrade-insecure-requests': '1', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', priority: 'u=0, i' }, [Symbol(kHeadersCount)]: 26, [Symbol(kTrailers)]: null, [Symbol(kTrailersCount)]: 0 } <ref *2> IncomingMessage { _events: { close: undefined, error: undefined, data: undefined, end: undefined, readable: undefined }, _readableState: ReadableState { highWaterMark: 16384, buffer: [], bufferIndex: 0, length: 0, pipes: [], awaitDrainWriters: null, [Symbol(kState)]: 1315084 }, _maxListeners: undefined, socket: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _events: { close: [Array], error: [Function: socketOnError], prefinish: undefined, finish: undefined, drain: [Function: bound socketOnDrain], data: [Function: bound socketOnData], end: [Array], readable: undefined, timeout: [Function: socketOnTimeout], resume: [Function: onSocketResume], pause: [Function: onSocketPause] }, _readableState: ReadableState { highWaterMark: 16384, buffer: [], bufferIndex: 0, length: 0, pipes: [], awaitDrainWriters: null, [Symbol(kState)]: 193997060 }, _writableState: WritableState { highWaterMark: 16384, length: 0, corked: 0, onwrite: [Function: bound onwrite], writelen: 0, bufferedIndex: 0, pendingcb: 0, [Symbol(kState)]: 17563908, [Symbol(kBufferedValue)]: null, [Symbol(kWriteCbValue)]: [Function (anonymous)], [Symbol(kAfterWriteTickInfoValue)]: null }, allowHalfOpen: true, _maxListeners: undefined, _eventsCount: 8, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, _server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, parser: HTTPParser { '0': null, '1': [Function: parserOnHeaders], '2': [Function: parserOnHeadersComplete], '3': [Function: parserOnBody], '4': [Function: parserOnMessageComplete], '5': [Function: bound onParserExecute], '6': [Function: bound onParserTimeout], _headers: [], _url: '', socket: [Circular *1], incoming: [Circular *2], outgoing: null, maxHeaderPairs: 2000, _consumed: true, onIncoming: [Function: bound parserOnIncoming], joinDuplicateHeaders: null, [Symbol(resource_symbol)]: [HTTPServerAsyncResource] }, on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Circular *1], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: null, [Symbol(errored)]: null, [Symbol(kHighWaterMark)]: 16384, [Symbol(kRejectNonStandardBodyWrites)]: false, [Symbol(kUniqueHeaders)]: null }, timeout: 0, [Symbol(async_id_symbol)]: 5, [Symbol(kHandle)]: TCP { reading: true, onconnection: null, _consumed: true, [Symbol(owner_symbol)]: [Circular *1] }, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: Timeout { _idleTimeout: -1, _idlePrev: null, _idleNext: null, _idleStart: 2299, _onTimeout: null, _timerArgs: undefined, _repeat: null, _destroyed: true, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 18, [Symbol(triggerId)]: 14 }, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(shapeMode)]: true, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: false, rawHeaders: [ 'Host', '127.0.0.1:8080', 'User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0', 'Accept', 'image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5', 'Accept-Language', 'en-US,en;q=0.5', 'Accept-Encoding', 'gzip, deflate, br, zstd', 'DNT', '1', 'Connection', 'keep-alive', 'Referer', 'http://127.0.0.1:8080/', 'Sec-Fetch-Dest', 'image', 'Sec-Fetch-Mode', 'no-cors', 'Sec-Fetch-Site', 'same-origin', 'Priority', 'u=6' ], rawTrailers: [], joinDuplicateHeaders: null, aborted: false, upgrade: false, url: '/favicon.ico', method: 'GET', statusCode: null, statusMessage: null, client: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _events: { close: [Array], error: [Function: socketOnError], prefinish: undefined, finish: undefined, drain: [Function: bound socketOnDrain], data: [Function: bound socketOnData], end: [Array], readable: undefined, timeout: [Function: socketOnTimeout], resume: [Function: onSocketResume], pause: [Function: onSocketPause] }, _readableState: ReadableState { highWaterMark: 16384, buffer: [], bufferIndex: 0, length: 0, pipes: [], awaitDrainWriters: null, [Symbol(kState)]: 193997060 }, _writableState: WritableState { highWaterMark: 16384, length: 0, corked: 0, onwrite: [Function: bound onwrite], writelen: 0, bufferedIndex: 0, pendingcb: 0, [Symbol(kState)]: 17563908, [Symbol(kBufferedValue)]: null, [Symbol(kWriteCbValue)]: [Function (anonymous)], [Symbol(kAfterWriteTickInfoValue)]: null }, allowHalfOpen: true, _maxListeners: undefined, _eventsCount: 8, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, _server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, requireHostHeader: true, joinDuplicateHeaders: undefined, rejectNonStandardBodyWrites: false, _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, _listeningId: 1, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, highWaterMark: 16384, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8080', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 2, [Symbol(kUniqueHeaders)]: null, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 22, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 4, [Symbol(triggerId)]: 3 } }, parser: HTTPParser { '0': null, '1': [Function: parserOnHeaders], '2': [Function: parserOnHeadersComplete], '3': [Function: parserOnBody], '4': [Function: parserOnMessageComplete], '5': [Function: bound onParserExecute], '6': [Function: bound onParserTimeout], _headers: [], _url: '', socket: [Circular *1], incoming: [Circular *2], outgoing: null, maxHeaderPairs: 2000, _consumed: true, onIncoming: [Function: bound parserOnIncoming], joinDuplicateHeaders: null, [Symbol(resource_symbol)]: [HTTPServerAsyncResource] }, on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Circular *1], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, [Symbol(shapeMode)]: false, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: null, [Symbol(errored)]: null, [Symbol(kHighWaterMark)]: 16384, [Symbol(kRejectNonStandardBodyWrites)]: false, [Symbol(kUniqueHeaders)]: null }, timeout: 0, [Symbol(async_id_symbol)]: 5, [Symbol(kHandle)]: TCP { reading: true, onconnection: null, _consumed: true, [Symbol(owner_symbol)]: [Circular *1] }, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: Timeout { _idleTimeout: -1, _idlePrev: null, _idleNext: null, _idleStart: 2299, _onTimeout: null, _timerArgs: undefined, _repeat: null, _destroyed: true, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 18, [Symbol(triggerId)]: 14 }, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(shapeMode)]: true, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _consuming: false, _dumped: false, [Symbol(shapeMode)]: true, [Symbol(kCapture)]: false, [Symbol(kHeaders)]: { host: '127.0.0.1:8080', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0', accept: 'image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5', 'accept-language': 'en-US,en;q=0.5', 'accept-encoding': 'gzip, deflate, br, zstd', dnt: '1', connection: 'keep-alive', referer: 'http://127.0.0.1:8080/', 'sec-fetch-dest': 'image', 'sec-fetch-mode': 'no-cors', 'sec-fetch-site': 'same-origin', priority: 'u=6' }, [Symbol(kHeadersCount)]: 24, [Symbol(kTrailers)]: null, [Symbol(kTrailersCount)]: 0 }

Backend

  • We can get a bit more excitement with our beloved command 'curl' C:\Users\cd-desk> curl http://localhost:8080 StatusCode : 200 StatusDescription : OK Content : {72, 101, 108, 108...} RawContent : HTTP/1.1 200 OK Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked Date: Tue, 30 Jul 2024 05:07:50 GMT Hello World! Headers : {[Connection, keep-alive], [Keep-Alive, timeout=5], [Transfer-Encoding, chunked], [Date, Tue, 30 Jul 2024 05:07:50 GMT]} RawContentLength : 12

Files cannot be 'curl'ed - curl here finds a live, HTTP-compliant server.

Backend

  • Basically, node server.js is serving utf-8/ascii
  • Review the snippet: Content : {72, 101, 108, 108...}
  • Those are the character codes for 'Hello World!' >>> vals = [72, 101, 108, 108] >>> [chr(v) for v in vals] ['H', 'e', 'l', 'l'] >>>
  • Takeaway: We can transmit data over HTTP.

Running Example: Wordle

    Wordle...

    • We habe an HTML webpage that reads and displays text.
    • We haven't looked at CSS yet, that's okay.
    • We have a script that processes text on the HTML page.
    • We have a remote server that transmits text.

    As I see it...

    • index.html
    • styles.css
    • script.js
    • server.js

    Let's connect the backend (server) to the frontend (everything else).

APIs

  • Wordle renders everything in capital letters.
    • 'Capital letters' is not a reference to the Fifty Shades Freed official soundtrack
    The toUpperCase() method of String values returns this string converted to uppercase.
  • This is a silly example, but let's
    • Transmit the input word to the server,
    • Capitalize it there, then...
    • Transmit it back.
  • But first, let's read anything from the server at all.
  • Many ways, this one feels the best to me, adapted from Stack Overflow async function readFromRemote() { const msg = await fetch("https://cat-fact.herokuapp.com/facts") ; const jsn = await msg.json() ; console.log(jsn) ; }
  • A lot going on here.

async

  • A lot going on here, from Stack Overflow
  • async function readFromRemote() { const msg = await fetch("https://cat-fact.herokuapp.com/facts") ; const jsn = await msg.json() ; console.log(jsn) ; }
  • First, the declaration: async function readFromRemote() {
  • We introduce the 'async' keyword. Why?
    • Usually in .js, one line of code is run, and the browser/node immediately proceeds to the next.
    • When dealing with a remote server, it could take some time get a message back.
    • So, async let's the browser/node know that it might need to a wait a minute.

await

  • A lot going on here, from Stack Overflow
  • async function readFromRemote() { const msg = await fetch("https://cat-fact.herokuapp.com/facts") ; const jsn = await msg.json() ; console.log(jsn) ; }
  • Then, fetch const msg = await fetch("https://cat-fact.herokuapp.com/facts") ;
  • Fetch is mostly used for apis.
    • It returns something called a promise, which isn't a value, but could be someday.
    • In the async environment, we can use 'await' to hang around until the promise resolves to value.
    • Vocab word: promise
    • The rest is just usually .js / programming, with a single url argument.
  • Cat fact seems to have gone offline while working on this presentation, but you can try other APIs.
  • This is why we 'await' - you never know if the remote server is actually there, or not!

Bonus: Why is 'msg' a 'const'?

json

  • A lot going on here, from Stack Overflow
  • async function readFromRemote() { const msg = await fetch("https://cat-fact.herokuapp.com/facts") ; const jsn = await msg.json() ; console.log(jsn) ; }
  • Then, we unpack the message. const jsn = await msg.json() ;
  • As we've seen from console.log() and 'curl', messages can be a lot.
  • We can follow the convention of tying them up in neat .json packages for transmition.
    • We do still have to await here - you can try out what happens if you don't.
    • We don't have to use json, its just a convention (and with good reason).

json

  • A lot going on here, from Stack Overflow
  • async function readFromRemote() { const msg = await fetch("https://meowfacts.herokuapp.com/") ; const jsn = await msg.json() ; const fct = jsn['data']; alert(fct) ; }
  • We can even just attach 'readFromRemote()' to a button.

server.js

  • We can just as easily read from our own server.js instance running via node at localhost:8080
  • async function readFromRemote() { const msg = await fetch("https://localhost:8080") ; }
  • But we'll enounter two problems.
    • We aren't yet transmitting in json, and really we should be, and...
    • For well-formed security reasons, .js is extremely nervous about running locally.
  • We need to jsonify and we need to signal we are okay with some insecure behavior, just for now.

server.js

  • Here was our server.js const http = require('http'); function serverFunc(req, res) { res.write('Hello World!') ; res.end() ; } serverObj = http.createServer(serverFunc) ; serverObj.listen(8080) ;
  • Let's switch to json!

via Docker

Naive function serverFunc(req, res) { res.write('Hello World!') ; res.end() ; }

json-ly function serverFunc(req, res) { res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({ data: 'Hello World!', })) ; }) ;


  • That's a little cramped, let's look at it all together.

server.js

  • Here is the json-ly server.js const http = require('http'); function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; } serverObj = http.createServer(serverFunc) serverObj.listen(8080);
  • Let's examine it.

server.js

  • Here is the json-ly server.js function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • Let's examine it. const hdr = {'Content-Type': 'application/json'} ;
  • With HTML and TSV files, we always had a header in the first line.
  • An HTTP response isn't so different from a file.
  • So we add a header specifying file type - in this case, json.
  • It's application data because it can be used in application directly by .js, Read more..
  • Note this is meta-json - it's json about json. If we sent an image, the header would still be in json.

server.js

  • Here is the json-ly server.js function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • Let's examine it. const str = {'data': 'Hello World!'} ;
  • I packaged my string as a data field in a small .json
  • Do whatever you want!
  • Just remember you'll be reading this on the other end of the wire, in script.js.

server.js

  • Here is the json-ly server.js function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • Let's examine it. const jsn = JSON.stringify(str) ;
  • As with text, .js has robust built-in support for JSON.
  • This is the most common way, to my knowledge, to create a .json payload, but there's at least several others that work quite well.
  • Read more here

server.js

  • Here is the json-ly server.js function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • Let's examine it. res.writeHead(200, hdr);
  • A'200' is the default HTTP status, and often needs to be included in any non-trivial message.
  • We also include our content-type notice here.
  • A response header is not unlike an HTML webpage head - it's useful information about the main body of the message.
  • This header will help fetch() understand what it is reading over in script.js

server.js

  • Here is the json-ly server.js function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • Let's examine it. res.end(jsn) ;
  • We end and send off the message with jsn object within.
  • You can also write then end, etc, but this is common for transmitting a discrete object.

We are close... but we still have to address our security concern.

server.js

  • Here is the UNSAFE json-ly server.js function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • Let's examine it. 'Access-Control-Allow-Origin': '*'
  • Adding this to the header also a backend and frontend to communicate over any (*) network.
  • Don't do this in real life, but it's fine in class.

server.js

  • Here is the UNSAFE json-ly server.js function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'} ; const str = {'data': 'Hello World!'} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • This would amicably interface with the following, in script.js async function readFromRemote() { const msg = await fetch("https://localhost:8080") ; // was: meowfacts const jsn = await msg.json() ; const fct = jsn['data']; alert(fct) ; }
  • Test it!

server.js

  • Our initial test case though was not see 'hello world', but to actually meaningfully compute on a remote server.
  • This is a silly example, but let's
    • Transmit the input word to the server,
    • Capitalize it there, then...
    • Transmit it back.
  • Remember reading input? async function readFromRemote() { const inEle = document.getElementById('inputElement') ; const inStr = inEle.value ; const msg = await fetch("https://localhost:8080") ; // was: meowfacts const jsn = await msg.json() ; const fct = jsn['data']; alert(fct) ; }
  • Wait... how to we communicate with the server?

server.js

  • Do a bit of testing.
    • Rather than just access localhost:8080, add something to the 'url'
    • Specifically, I added a word to the end: something easy to find, like 'aegis'
    • Then I console.log() the entire request within server.js
    • Then I read the entire log looking for aegis.
    • Remember reading input? function serverFunc(req, res) { console.log(req) ; }
    • I start the server and capture its output to a file. node server.js > out.txt
    • In a separate window, I curl the server. curl 127.0.0.1:8080/aegis
    • Then I can simply search the output for 'aegis': url: '/aegis'
    • I found this in line 272 with an editor, grep seemed unstable on binary json data.
      • Apparently there's json specific utilities. I say: out of scope.

server.js

  • Do a bit of testing.
    • Apparently that request is a .json file, so let's take advantage of that: function serverFunc(req, res) { console.log(req['url']) ; }
    • I start the server as is, just to see. node server.js
    • In a separate window, I curl the server. curl 127.0.0.1:8080/aegis
    • Here's what I see (just happened to use powershell, it doesn't matter): PS C:\dev> node server.js /aegis
    • Oh, that's actually... extremely nice.

server.js

  • We change 'Hello World!' to 'req['url'].toUpperCase() function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'} ; const str = {'data': req['url'].toUpperCase()} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • We append the input word after a single '/' to the target url. async function readFromRemote() { const inEle = document.getElementById('inputElement') ; const inStr = inEle.value ; const msg = await fetch("https://localhost:8080/" + inEle.value) ; const jsn = await msg.json() ; const fct = jsn['data']; alert(fct) ; }
  • Test it!

server.js

  • I tested with curl. PS C:\Users\cd-desk> curl 127.0.0.1:8080/aegis StatusCode : 200 StatusDescription : OK Content : {"data":"/AEGIS"} RawContent : HTTP/1.1 200 OK Access-Control-Allow-Origin: * Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked Content-Type: application/json Date: Tue, 30 Jul 2024 21:48:27 GMT {"dat...
  • This is known as "cloud computing" (especially if node is invoked on AWS/AZ/GCP).

server.js

  • Wordle is a bit more involved than case change, so write a helper function. function serverFunc(req, res) { const hdr = {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'} ; const str = {data: wordleHelper(req['url'])} ; const jsn = JSON.stringify(str) ; res.writeHead(200, hdr); res.end(jsn) ; }
  • Then we can write a function that, given an input word and target word, returns a wordle coloration. const tword = 'blimp' // ChatGPT said this was the funniest possible word function wordleHelper(iword) { // What I did: // return g for inplace, y for inword, r for nothing // return all x's for non-word return "rrgyr" ; // for e.g. 'trips' }
  • I leave this as an exercise to the interested student. There's a wordlist here.

script.js

  • We can return now to script.js.
  • async function readFromRemote() { const inEle = document.getElementById('inputElement') ; const inStr = inEle.value ; const msg = await fetch("https://localhost:8080/" + inEle.value) ; const jsn = await msg.json() ; const gyr = jsn['data']; // would be e.g. "rrgyr" for 'trips' updateBoard(inStr, gyr) ; }
  • Now we simply need to style the letters in e.g. 'trips' within the table element 'gameboard' function updateBoard(word, gyr) { const inEle = document.getElementById('gameboard') ; // What next? }

script.js

  • We utilize 'innerHTML' to populate a table. function updateBoard(word, gyr) { const inEle = document.getElementById('gameboard') ; // update innerHTML here. }
  • Let's look at a minimal HTML table example, from MDN <table> <tr> <th>Data 1</th> <th style="background-color: yellow">Data 2</th> </tr> <tr> <td>Calcutta</td> <td style="background-color: yellow">Orange</td> </tr> <tr> <td>Robots</td> <td style="background-color: yellow">Jazz</td> </tr> </table>

script.js

    <table> <tr> <th>Data 1</th> <th style="background-color: yellow">Data 2</th> </tr> <tr> <td>Calcutta</td> <td style="background-color: yellow">Orange</td> </tr> <tr> <td>Robots</td> <td style="background-color: yellow">Jazz</td> </tr> </table>
  • Gives:
    Data 1 Data 2
    Calcutta Orange
    Robots Jazz

script.js

  • Basically, a table element wraps tr (table row) elements that wrap td (table data) elements.
  • We don't need a header, so I take it out. <table> <tr> <td>Calcutta</td> <td style="background-color: yellow">Orange</td> </tr> <tr> <td>Robots</td> <td style="background-color: yellow">Jazz</td> </tr> </table>
  • Gives:
    Calcutta Orange
    Robots Jazz

script.js

  • So given a word and a coloration/styling, we need to generate a 'tr' with 5 internal 'td's
  • We need appropriate styling for the 'td's.
  • It's simple enough to begin with no styling.
  • Let's go to script.js and define a check function.
  • We'll just read from two inputs for now. function check() { const inEle1 = document.getElementById('inputElement1').value ; const inEle2 = document.getElementById('inputElement2').value ; let newInner = "" ; for (let i = 0; i < 5; i++) { newInner += '<td>' + inEle1[i] + '</td>' } document.getElementById('gameboard').innerHTML = newInner ; }}
  • This isn't styled at all, but it can be.

styles.css

  • To get this looking more wordle-like, I went to the wordle site with a color picker.
  • Anyways, you can edit this however you like but I used: td { border: 1px solid #121213 ; width: 64px; height: 64px; font-size: 200%; font-family: monospace; font-weight: bold; text-align: center; color: white; } .g { /* _g_reen. */ background-color: #538D4E ; } .y { /* _y_ellow */ background-color: #B59F3B ; } .r { /* g_r_ay. I thought misses were in red not in gray - whoops. */ background-color: #3A3A3C ; }

styles.css

  • To get this looking more wordle-like, I went to the wordle site with a color picker.
  • Anyways, you can edit this however you like but I used: td { border: 1px solid #121213 ; width: 64px; height: 64px; font-size: 200%; font-family: monospace; font-weight: bold; text-align: center; color: white; }
  • This part:
    • Outlines every table cell in a dark gray
    • Makes boxes squares using pixel values (but won't scale well on different screen, I'd assume)
    • Makes the font big and bold, and makes letters the same width.
    • Centers letters in their boxes, and makes the letters appear in white.

styles.css

  • To get this looking more wordle-like, I went to the wordle site with a color picker.
  • Anyways, you can edit this however you like but I used: .g { /* _g_reen. */ background-color: #538D4E ; } .y { /* _y_ellow */ background-color: #B59F3B ; } .r { /* g_r_ay. I thought misses were in red not in gray - whoops. */ background-color: #3A3A3C ; }
  • This part:
    • Defines names of three class, 'g','y', and 'r'
    • Gives these colors chosen by NYT to show up high contrast on their dark background and white letters.

styles.css

  • Since I use tables elswhere in my slides, it's relatively goofy to embed this...
  • So this isn't going to look great...
  • But you'll see the point. function check() { const inEle1 = document.getElementById('inputElement1').value ; const inEle2 = document.getElementById('inputElement2').value ; let newInner = "" ; for (let i = 0; i < 5; i++) { newInner += '<td class=\'' + inEle2[i] + '\'>' + inEle1[i] + '</td>' } document.getElementById('gameboard').innerHTML = newInner ; }}
  • This isn't touching node, but it could be.

Demo