> [!NOTE] Heads up! > These are just random notes as I read a book about writing an http server ## Implement an ECHO server Here is the implementation of a simple echo server: ```ts // import the net module import * as net from "net"; function newConn(socket: net.Socket) { console.log("new connection", socket.remoteAddress, socket.remotePort); socket.on("end", () => { console.log("FIN"); }) socket.on("data", (data: Buffer) => { console.log(data); socket.write(data); }) if (data.includes("q")) { console.log("closing"); socket.close(); } } // create a new server to listen let server = net.createNetServer(); // register runtime callbacks for events server.on("connection", newConn); // what to do when new connection establisehd server.on("end", () => { console.log("FIN"); }) server.on("error", (error: Error) => { throw error; }) ``` ## The Event Loop in JS The event loop in JS is basically a giant while true loop that first checks for events, then calls the callbacks that were registered to those events. In pseudocode this would look like: ```js while(true) { let events = check_for_events(); for (let e of events) { do_callback(e); } } ``` ## Converting the Callback Echo Server to Promised based Ok I've read the section on this and now I want to make sure I can write about it well and in detail. So the goal is take the echo server (shown above) and rather than use the callbacks to do async we want to use the promised-based approach. One thing that is important to re-iterate is that we can do async programming with either callbacks or with async/await keywords. In simplest terms a function that conforms to the callback API takes the form of ```ts function server() { do_action_for_event((error, result) => { if (error) { // do something } else { // do something else } }) } ``` on the other hand a promise based one would look something like: ```ts function do_action_for_event(): Promise<T>; function server() { try { const result: T = await do_something_for_event() } catch (error) { // do something on fail } } ``` the big deal here is that with the callback you can end up in a heavily nested mess of callbacks, with the promise based api you can more or less keep the structure of your code much more imperative, which I happen to agree is a good thing, under the hood though its just callbacks. ### What is a Promise? promise is an object in JS that allows us to run async code in an imperative structure. It does by essentially giving us a "handle" to the object that represents code that will eventually resolve or be rejected. At any point in time a promise can be in three different states 1. pending - still waiting for operation to complete 2. fulfilled - the operation completed successfully 3. rejected - the operation failed Here is the structure of a promise. ```ts function do_something_for_event_promise(): Promise<T> { return new Promise<T>((resolve, reject) => { do_something_for_event((err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } ``` Let's break it down: The outer-most function `do_something_for_event_promise` is a function that returns a Promise object. When we call this function if we don't want to block we would use await if within an async function or the `.then` / `.catch` syntax. ```ts do_something_for_event_promise() .then(res => {console.log("done")}) .catch(error => {console.log("oops")}) ``` Next we have the return which is also the construction of the promise object. As mentioned before the Promise constructor takes in an "executor" which itself takes in two callbacks `resolve` and `reject` as its arguments. The executor in our example is: ```ts (resolve, reject) => { do_something_for_event((err, result) => { if (err) { reject(err); } else { resolve(result); } }); } ``` These two callbacks are then the functions used to either fulfill or reject the operation. ### Echo Server to promise-based We start the process by creating a new type to represent our tcp connection ```ts type TCPConn = { socket: net.Socket; reader: null | { resolve: (value: Buffer) => void; reject: (reason: Error) => void; }; err: null | Error; ended: boolean; }; ``` our new type is composed of a few elements: - socket - not surprising we still need a `net.Socket` to listen and write to - reader - will either be null (when socket is first created or paused) or will store the callbacks for the `resolve` and `reject` (provided to us by the runtime) that the promise-based api will use. The resolve function will take in some buffer data, and the reject function will take an Error. - err - an error object that we can throw - ended - a boolean indicating whether or not our connection is ended Next we create a function for initializing our socket, it will return a new `TCPConn` object as well as register callbacks to our socket for the events, "data", "error" and "ended". ```ts function socketInit(socket: net.Socket): TCPConn { const conn: TCPConn = { socket: socket, reader: null, err: null, ended: false, }; socket.on("data", (data: Buffer) => { console.assert(conn.reader); conn.socket.pause(); conn.reader!.resolve(data); conn.reader = null; }); socket.on("error", (err: Error) => { conn.err = err; if (conn.reader) { conn.reader.reject(err); conn.reader = null; } }); socket.on("ended", () => { conn.ended = true; if (conn.reader) { conn.reader.resolve(Buffer.from("")); conn.reader = null; } }); return conn; } ``` Lets take apart each of the event callbacks. #### Data Event ```ts socket.on("data", (data: Buffer) => { console.assert(conn.reader); conn.socket.pause(); conn.reader!.resolve(data); conn.reader = null; }); ``` The first thing we do is take care of handling what happens when we get a data event. We first assert that we have the resolve and reject callbacks registered in our `conn` object. We then proceed to pause the the socket connection from getting new data, invoke the resolve callback. Lastly we set the reader to null, since the promise has been fulfilled. While the socket is paused any incoming data will be kept on the stack by the OS so whenever we resume data will not be lost. #### Error Event This is pretty straighforward if we get the error event and the promise executor has been registered then we call the promises's reject callback. #### Data For data we expect to have data to process this is of course type `Buffer`.