Graduate Program KB

Introduction to Node.js

  • Node.js is a JavaScript runtime environment designed to handle asynchronous events efficiently

  • It uses the standard I/O streams, stdin, stdout and stderr

    process.stdin.read();
    process.stdout.write();
    process.stderr.write();
    
    • console.log could be described as a wrapper around process.stdout.write that appends a newline
    • console.error is also similar, but for process.stderr.write
    • Preferably don't use process.stdin.read, it has more quirks compared to the output streams due to the interactive shell
      • Use an already existing package for reading input

Command line scripts

  • Add the following line to the top of a script file:

    #!/usr/bin/env node
    
    • Locates node within the system and then uses node to interpret the rest of the program
  • Run the code in strict mode

    "use strict";
    
  • Add executable permissions if there are none

    chmod u+x {file}
    
  • Access arguments passed from the shell

    • process.argv contains all arguments passed to the script

    • process.argv[0] and process.argv[1] is the path to invoke node and the path to our file respectively

    • process.argv.slice(2) contains all other arguments after the second index

    • Minimist is a library for processing arguments

    • Returns an object with the following structure: { _: [], <parameter>:<value>, ... }

    • The number of <parameter>:<value> pairs in the object is arbitrary but of course, less than the number of arguments

    • _ is a key to an array which holds arguments that Minimist does not know how to parse

    let args = require("minimist");
    
    args(process.argv.slice(2));
    
    // Configure arguments to data types
    args(process.argv.slice(2), {
        boolean: ["help"],
        string: ["file"]
    });
    
  • Working with files:

    • __dirname is a variable that contains the absolute path of the currently executing script
    • process.cwd() returns current working directory
    • Synchronously read from file, expects a callback function now
    let path = require("path");
    let fs = require("fs");
    
    let filePath = path.resolve(file);
    let contents = fs.readFileSync(filePath);
    
    function callback(error, contents) {
        if (error) {
            process.stderr.write(error);
        }
        else {
            process.stdout.write(contents);
        }
    }
    
    fs.readFile(filePath, callback);
    
    • Asynchronously read from file
    fs.readFile(filePath);
    

Streams

  • Simplex streams are unidirectional, meaning they are either readable or writable

  • Duplex streams are readable and writable

    let path = require("path");
    let fs = require("fs");
    
    let readStream = fs.createReadStream(filePath);
    let writeStream = fs.createWriteStream(filePath);
    
    let outStream = readStream;
    outStream.pipe(process.stdout); // Writes input to stdout
    
  • Transform streams intercept the pipe to process items one at a time

    • Chunk is a buffer from the stream
    • The callback function notifies the transform is complete
    let Transform = require("stream").Transform;
    
    let upperStream = new Transform({
        transform(chunk, encoding, callback) {
            this.push(chunk.toString().toUpperCase());
            callback();
        }
    });
    
    let outStream = outStream.pipe(upperStream);
    outStream.pipe(process.stdout);
    
  • GZIP compress with ZLIB

  • Unzipping, processing then rezipping files transferred over some protocol

    • Create zipping and unzipping streams
    • Can change file type of the compressed file to .gz
    let zlib = require("zlib");
    
    let gzipStream = zlib.createGzip();
    let outStream = outStream.pipe(gzipStream);
    outputFile = `{outputFile}.gz`; 
    
    let gunzipStream = zlib.createGunzip();
    let outStream = outStream.pipe(gunzipStream);
    
  • Determine the end of a stream

    function streamComplete(stream) {
        return new Promise(function callback(response) {
            stream.on("end", response);
        });
    }
    
    outStream.pipe(process.stdout);
    await streamComplete(outStream);
    

Database

  • SQLite3 can be maintained directly by the application and not externally
    let path = require("path");
    let fs = require("fs");
    let sqlite3 = require("sqlite3");
    
    const DB_PATH = path.join(__dirname, "my.db");
    const DB_SQL_PATH = path.join(__dirname, "mydb.sql");
    
    // Some helpers
    let myDb = new sqlite3.Database(DB_PATH);
    let SQL3 = {
        run(...args) {
            return new Promise(function callback(resolve, reject) {
                myDB.run(...args, function onResult(error) {
                    if (error) {
                        reject(error);
                    }
                    else {
                        resolve(this);
                    }
                });
            });
        },
        // Return promise interface for each callback
        // Documentation: https://github.com/TryGhost/node-sqlite3/wiki/API
        get: util.promisify(myDB.get.bind(myDB)),
        all: util.promisify(myDB.all.bind(myDB)),
        exec: util.promisify(myDB.exec.bind(myDB))
    };
    

Web Servers

  • Set up web server and handle requests to the server

    let http = require("http");
    
    const HTTP_PORT = 8039;
    
    async function handleRequest(request, respones) {
        if (request.url == "/hello") {
            // Write header and closes response stream
            // The "Hello World!" of HTTP servers
            response.writeHead(200, { "Content-Type": "text/plain" });
            response.end("Hello World!");
        }
        else {
            response.writeHead(404);
            response.end();
        }
    }
    
    // Instantiate server
    let httpServer = http.createServer(handleRequest);
    
    httpServer.listen(HTTP_PORT);
    
  • Configure a server to only allow access to files under the web root, adds a file server that uses regex to match incoming requests

    let path = require("path");
    let http = require("http");
    let staticAlias = require("node-static-alias");
    
    const WEB_PATH = path.join(__dirname, "web");
    const HTTP_PORT = 8039;
    
    let fileServer = new staticAlias.Server(WEB_PATH, {
        // Time to cache files in seconds
        cache: 100,
        serverInfo: "Server name",
        alias: [
            {
                // If the URL contains index (excludes HTTP portion at the start)
                match: /^\/(?:index\/?)?(?:[?#].*$)?$/,
                serve: "index.html",
                force: true
            },
            {
                // If the URL contains .js
                match: /^\/js\/.+$/,
                serve: "<% absPath %>",
                force: true
            },
            {
                // If the URL contains words or numbers followed by separators
                match: /^\/(?:[\w\d]+)(?:[\/?#].*$)?$/,
                serve: function onMath(params) {
                    return `${params.basename}.html`;
                }
                force: true
            },
            {
                match: /[^]/,
                serve: "404.html"
            }
        ]
    });
    
    async function handleRequest(request, respones) {
        fileServer.serve(request, response);
    }
    
    // Instantiate server
    let httpServer = http.createServer(handleRequest);
    
    httpServer.listen(HTTP_PORT);
    
  • Set up server to respond to API requests at a given endpoint

    async function handleRequest(request, response) {
        // API request
        if (request.url == "/get-records") {
            // Get records from database
            let records = await getAllRecords();
    
            response.writeHead(200, {
                "Content-Type": "application/json",
                "Cache-Control": "no-cache"
            });
            response.end(JSON.stringify(records));
        }
        // Non-API requests
        else {
            fileServer.serve(request, response);
        }
    }
    
    // Instantiate server
    let httpServer = http.createServer(handleRequest);
    
    httpServer.listen(HTTP_PORT);
    
  • Modify server to use Express.js instead of manual routing

    • Set up middleware, which is a function that gets called if the incoming request matches some criteria
    • The order of middleware routes defined matters since they are executed in order
    • Call next() within the middleware function if you want it to execute the next middleware functions other than the first
    let path = require("path");
    let http = require("http");
    let express = require("express");
    
    const WEB_PATH = path.join(__dirname, "web");
    let app = express();
    
    
    function defineRoutes() {
        // GET requests
        app.get("/get-records", async function(request, response) {
            let records = await getAllRecords();
    
            response.writeHead(200, {
                "Content-Type": "application/json",
                "Cache-Control": "no-cache"
            });
            response.end(JSON.stringify(records));
        });
    
        //All incoming requests
        app.use(express.static(WEB_PATH, {
            maxAge: 100,
            setHeaders: function setHeaders(res) {
                res.setHeader("Server", "Server name");
            }
        }));
    }
    
    defineRoutes();
    let httpServer = http.createServer(app);
    

Child Processes

  • Create child processes and check its status
    let util = require("util");
    let childProcess = require("child_process");
    
    let child = childProcess.spawn("node", [ "file" ]);
    child.on("exit", function(code) {
        console.log("Child finished", code);
    })
    
    async function main() {
        try {
            let response = await fetch(`http://localhost:{HTTP_PORT}/get-records`);
    
            if (response && response.ok) {
                let records = await response.json();
    
                if (records && records.length > 0) {
                    process.excitCode = 0;
    
                    return;
                }
            }
        }
        catch(error) {
            ...
        }
    
        process.exitCode = -1
    }
    
    main();
    

Debugging

  • Use web developer tools to debug
    • In chrome://inspect, which listens to the port on localhost, can observe the server activity
    • Run with --inspect to broadcast the port on localhost