Test Category

Test Blog Post

Starter template for writing out a blog post using MDX/JSX and Next.js.

No Name Exists

Abdullah Muhammad

Published on May 17, 20265 min read 2 views

Share:
Article Cover Image

Introduction

Building off what we covered in the last tutorial, we will walkthrough a web application which utilizes the LRU Cache data structure.

The LRU Cache will be implemented in the back-end and it will be used to create, read, update, and delete user tasks.

Without further ado, let us dive right in. You can clone the repository here. The directory of concern is /demos/Demo36_LRUCache_Implementation.

The web application is a simple task handler that enables the four CRUD operations.

Code Overview

The full-stack application uses React/Node.js for the front/back-end implementation.

There can only be a maximum of five tasks before the least prioritized task is deleted (following the LRU Cache principle).

The user can perform CRUD operations to modify the cache and the contents of its data are written to a persistent file stored in /backend/database/items.json.

This serves as a mock database and is sufficient for the scope of this web application.


LRU Cache

The following is the implementation of the LRU Cache /backend/dataTypes/LRUCache.ts:

GitHub GistTypeScript
// Least Recently Used Cache (LRU)
// Most recent items are added to the back
// Least Recently Used items fall to the front
export default class LRU {
    private orderArray: any[];
    private capacity: number;
    private keyMap = new Map();
    
    // Initialize members and set capacity, must be at least 1
    constructor(c: number) {
        this.capacity = c <= 0 ? 1 : c;
        this.orderArray = [];
    }

    // Check to see if LRU Cache contains key
    // Check if key is defined, but is expired (key value set to undefined)
    has(key: any): boolean {
        if (this.keyMap.has(key) && (this.keyMap.get(key) === undefined)) {
            return false;
        }
        else if (this.keyMap.has(key)){
            return true;
        }
        else {
            return false;
        }
    }

    // Insert key and its value based on LRU Cache memory
    set(key: any, value: any): void {
        // Check if LRU Cache is at capacity
        if ((this.orderArray.length === this.capacity) && (!this.keyMap.has(key))){
            // Perform eviction only if unique key is to be added
            // Delete key from map
            let evictedValue = this.orderArray[0];
            this.keyMap.delete(evictedValue);
            this.orderArray.splice(0, 1);

            // Check to see if value already exists
            // Expire current key
            let keysArray = Array.from(this.keyMap.keys());
            for (let i = 0; i < keysArray.length; i++){
                if (this.keyMap.get(keysArray[i]) === value) {
                    this.keyMap.set(keysArray[i], undefined);
                }
            }
            
            // Finally, add the new key-value pair
            this.orderArray.push(key);
            this.keyMap.set(key, value);
        }
        else if ((this.orderArray.length === this.capacity) && (this.keyMap.has(key))) {
            let filteredArray = this.orderArray.filter(existingKey => existingKey !== key);
            this.orderArray = filteredArray;

            // Check to see if value already exists
            // Expire current key
            let keysArray = Array.from(this.keyMap.keys());
            for (let i = 0; i < keysArray.length; i++){
                if (this.keyMap.get(keysArray[i]) === value) {
                    this.keyMap.set(keysArray[i], undefined);
                }
            }
            
            // Finally, add the new key-value pair
            this.orderArray.push(key);
            this.keyMap.set(key, value);
        }
        else {
            // Perform the same steps above without eviction
            // Check to see if value already exists
            let keysArray = Array.from(this.keyMap.keys());
            for (let i = 0; i < keysArray.length; i++){
                // Expire current key
                if (this.keyMap.get(keysArray[i]) === value) {
                    this.keyMap.set(keysArray[i], undefined);
                }
            }
                // Finally, add the new key-value pair
                this.orderArray.push(key);
                this.keyMap.set(key, value);
            }
    }

    // Retrieve key based on LRU Cache memory
    // Move key to the front of the Cache
    get(key: any) : any {
        if (this.keyMap.has(key)){
            let filteredArray = this.orderArray.filter(existingKey => existingKey !== key);
            this.orderArray = filteredArray;
            this.orderArray.push(key);

            return this.keyMap.get(key);
        }
        else {
            return -1;
        }
    }

    // Delete key from LRU Cache, but return its value
    delete(key: any): any {
        if (this.keyMap.has(key)){
            let filteredArray = this.orderArray.filter(existingKey => existingKey !== key);
            this.orderArray = filteredArray;
            let value = this.keyMap.get(key);
            this.keyMap.delete(key); // Remove key from order array as well as map
            
            return value; 
        }
        else {
            return -1;
        }
    }

    // Print LRU Cache
    print(): void {
        let consoleString = ' [ ';
        for (let i = 0; i < this.orderArray.length; i++){
            consoleString += ' ' + this.orderArray[i] + " => " + this.keyMap.get(this.orderArray[i]) + ', ';
        }
        consoleString = consoleString.substring(0, consoleString.length - 2); 
        consoleString += ' ] ';
        console.log(consoleString);
    }

    // Return the LRU cache
    getLRUCache(): Array<any> {
        return this.orderArray;
    }

    // Return the Key Map
    getLRUCacheKeyMap(): Map<any, any> {
        return this.keyMap;
    }
}
Custom LRU Cache implementation

Building on the implementation from the last tutorial, this is the LRU Cache implementation with two additional functions added for ease of use. The getLRUCache() function and the getLRUCacheKeyMap() function which simply return the order of keys and key map, respectively.


Utility Functions

There are utility functions that exist which provide modularity and ease of use when working with the controller functions.

The first one is the fileReader.ts module /backend/util/fileReader.ts:

GitHub GistTypeScript
import * as fs from 'fs';

// Read and return the LRU Cache data
export default async function fileReader() {
    try {
        let fileData = fs.readFileSync('./database/items.json');
        return JSON.parse(String(fileData)).LRU;
    }
    catch (err) {
        throw new Error("Could not read file");
    }
}
fileReader.ts module reads the items.json mock database and returns the LRU Cache data

It is a nice utility function in that, it provides all LRU Cache data stored in a persistent file which serves as the mock database /backend/database/items.json.


The second utility function is the fileWriter which writes the LRU Cache data back in the items.json file /backend/util/fileWriter.ts:

GitHub GistTypeScript
import * as fs from 'fs';
import LRU from '../dataTypes/LRUCache';
import ItemType from '../dataTypes/ItemType';

// Write the LRU cache back to the database
export default async function fileWriter(fileObject: Array<ItemType>){
    try {
        fs.writeFileSync('./database/items.json', JSON.stringify({ LRU : fileObject }));
    }
    catch(err) {
        throw new Error("Could not write to file");
    }
}
fileWriter.ts module writes the updated LRU Cache data to the items.json mock database

Similar to the fileReader function, the fileWriter function writes the data of the modified LRU Cache back into the database.

Both functions make use of the built-in fs module for file reading and writing out-of-the-box.


The final utility function /backend/util/setLRUCache.ts creates the LRU Cache with a capacity of five and adds in all the task IDs and their descriptions.

This serves to provide a copy of the actual LRU Cache stored in the items.json file for the purposes of modifications which are written back in the items.json file:

GitHub GistTypeScript
import LRUCache from '../dataTypes/LRUCache';
import ItemType from '../dataTypes/ItemType';

export default function setLRUCache(data: Array<ItemType>): LRUCache {
    let lru = new LRUCache(5); // Setting the LRU Cache capacity to 5

    // Check to see the length of object and traverse through it and set LRU Cache
    for (var i = 0; i < data.length; i++) {
        lru.set(data[i].id, data[i].description);
    }

    return lru;
}
setLRUCache.ts module creates a copy of the LRU Cache data structure from the items.json database

Aside from the utility functions, there are two custom types /backend/dataTypes:

  • Item — For defining task definitions by their ID and description
  • LRU Cache — Class for working with the LRU Cache data structure

We have already looked at the LRU Cache. The following is the Item type definition:

GitHub GistTypeScript
// Item type to be used for setting key-value pairs in the LRU Cache
export default interface ItemType {
    id: string,
    description: string
}
Item type definition for working with tasks

Pretty straight forward. Now, we focus on the main controller functions which enable the CRUD operations.


Controller Functions

The deleteTask function allows the user to delete any task based on their ID. The ID is generated using the uuid library and is sufficient for the scope of this web application.

The function makes use of the utility functions to read and set the LRU Cache. It proceeds to delete the requested task provided by the user and writes the modified LRU Cache back into the items.json file /backend/Controller/DeleteTaskController.ts:

GitHub GistTypeScript
import { Request, Response } from 'express';
import fileReader from '../util/fileReader';
import setLRUCache from '../util/setLRUCache';
import fileWriter from '../util/fileWriter';
import ItemType from '../dataTypes/ItemType';

export default async function deleteTask(req: Request, res: Response){
    const { deleteData } = JSON.parse(req.body.body);

    try {
        // Set LRU Cache and then proceed to delete entry
        // Re-write updated LRU Cache to database
        const fileData = await fileReader();
        let LRUCache = setLRUCache(fileData);
        let fileObject: ItemType[] = [];

        // Key, if exists, deleted from LRU Cache
        // Finally, write the LRU Cache back to database
        LRUCache.delete(deleteData);
        let keyMapKeys = Array.from(LRUCache.getLRUCacheKeyMap().keys());

        // Make use of the LRU Cache Key Map and iterate through it to access each key for its value
        for (let item = 0; item < keyMapKeys.length; item++){
            let task = LRUCache.getLRUCacheKeyMap().get(keyMapKeys[item]);
            fileObject.push({ id: keyMapKeys[item], description: task });
        }
        
        // Write modified LRU Cache back to database
        await fileWriter(fileObject);

        res.status(200).json({
            message: "Deletion successful"
        });
    }
    catch (err) {
        res.status(400).json({
            message: "Could not read file. ERROR: " + err
        });
    }
}
DeleteTaskController.ts file contains implementation for deleting tasks

The getTasks function simply reads through the items.json file and returns the keys (or in this case, IDs) of the tasks the user has created /backend/Controller/GetTasksController.ts:

GitHub GistTypeScript
import { Request, Response } from 'express';
import fileReader from '../util/fileReader';
import setLRUCache from '../util/setLRUCache';

// Fetch all task IDs from the LRU Cache
export default async function getTasks(req: Request, res: Response){
    try {
        const fileData = await fileReader();
        let keysList = setLRUCache(fileData).getLRUCache();

        // Return the LRU Cache containing only the keys
        res.status(200).json({
            LRUData: keysList
        });
    }
    catch (err) {
        res.status(400).json({
            message: "Could not fetch tasks"
        });
    }
}
GetTasksController.ts file contains the getTask function which returns the keys of the LRU Cache

The insertTask function enables the user to create a task by providing input text. The function makes use of the uuid library to create a random ID and assigns it to the task.

After that, the LRU cache is updated and the modified version is written back into the items.json file /backend/Controller/InsertTaskController.ts:

GitHub GistTypeScript
import { Request, Response } from 'express';
import fileReader from '../util/fileReader';
import fileWriter from '../util/fileWriter';
import setLRUCache from '../util/setLRUCache';
import ItemType from '../dataTypes/ItemType';
import { v4 as uuid } from 'uuid';

// LRU data is within the database and is stored as { LRU: [] }
export default async function insertTask(req: Request, res: Response){
    const { task } = JSON.parse(req.body.body);

    try {
        // Fetch file information and set the LRU Cache
        const readFileResponse = await fileReader();
        let LRUCache = setLRUCache(readFileResponse);

        // Add new key-value pair to the LRU Cache
        // Make use of the UUID library for unique keys
        // Write data back to database
        LRUCache.set(uuid().split("-")[0], task);
        let fileObject: ItemType[] = [];
        const keyMapKeys = Array.from(LRUCache.getLRUCacheKeyMap().keys());

        // Make use of the LRU Cache Key Map and iterate through it to access each key for its value
        for (let item = 0; item < keyMapKeys.length; item++){
            let task = LRUCache.getLRUCacheKeyMap().get(keyMapKeys[item]);
            fileObject.push({ id: keyMapKeys[item], description: task });
        }

        // Write modified LRU Cache to file
        await fileWriter(fileObject);

        res.status(200).json({
            message: "Insertion complete!"
        });
    }
    catch(err) {
        res.status(400).json({
            message: "Could not insert data. ERROR: " + err
        });
    }   
}
InsertTaskController.ts file containing implementation for inserting new tasks into the LRU Cache

Finally, the last of the CRUD operations is the update function. The updateTask function enables the user to update a description of an existing task and have it written back into the LRU cache.

You can find the implementation here /backend/Controller/UpdateTaskController.ts:

GitHub GistTypeScript
import { Request, Response } from 'express';
import fileReader from '../util/fileReader';
import fileWriter from '../util/fileWriter';
import setLRUCache from '../util/setLRUCache';
import ItemType from '../dataTypes/ItemType';

export default async function updateTask(req: Request, res: Response){
    const { updateID, description } = JSON.parse(req.body.body);

    try {
        // Set LRU Cache and then proceed to delete entry
        // Re-write updated LRU Cache to database
        const fileData = await fileReader();
        let LRUCache = setLRUCache(fileData);

        // Update key with new description
        // Finally, write the LRU Cache back to database
        LRUCache.set(updateID, description);

        let fileObject: ItemType[] = [];
        let keysArrangement = LRUCache.getLRUCache();
        const keyMapKeys = Array.from(LRUCache.getLRUCacheKeyMap().keys());

        // Make use of the LRU Cache Key Map and iterate through it to access each key for its value
        for (let item = 0; item < keyMapKeys.length; item++){
            let task = LRUCache.getLRUCacheKeyMap().get(keysArrangement[item]);
            fileObject.push({ id: keysArrangement[item], description: task });
        }

        // Write modified LRU Cache to file
        await fileWriter(fileObject);

        res.status(200).json({
            message: "Insertion complete!"
        });
    }
    catch (err) {
        res.status(400).json({
            message: "Could not update task. ERROR: " + err
        });
    }
}
UpdateTaskController.ts file containing the updateTask function for updating an existing task

The last controller function provides all the LRU Cache data to the user for viewing purposes and it can be found in /backend/Controller/ViewTasksController.js:

GitHub GistTypeScript
import { Request, Response } from 'express';
import fileReader from '../util/fileReader';

export default async function viewTasks(req: Request, res: Response){
    // Read file and return LRU data in reverse order
    try {
        const readFileData = await fileReader();
        res.status(200).json({
            LRUData: readFileData.reverse()
        });
    }
    catch(err) {
        res.status(400).json({
            message: "Could not read file. ERROR: " + err 
        });
    }
}
ViewTasksController.ts file reads the items.json file and returns all the LRU Cache data back to the user

Suffice to say, that is all we need from a back-end POV. By providing a custom implementation of the LRU cache, we enable separation of concerns and modularity.

We will not walk through the front-end implementation as it follows similar React implementations we have looked at in the past.

Feel free to look into it though. The client codebase is located in the /frontend portion of this directory.

Demo Time!

Assuming you cloned the repository, ensure you ran npm install in each of the front/back-end directories.

This will install all the necessary dependencies to work with the project. On two separate windows, from the root directory of this project, do the following:

  • Run npm start in /frontend to launch the front-end server
  • npx ts-node server.ts in /backend to launch the back-end server

Upon launch, you should see this:

No Image Found
Home page of the task handler application

Proceed to the Insert Task page and fill out a description for a task:

No Image Found
Insert task page for creating and inserting a task

Proceed to the View Tasks page and you should see your newly created task there:

No Image Found
View tasks page displaying the one and only task

You can see the uuid library was used to assign this task a unique ID. The table orders the tasks from the most recent to least recent.

Try to add five more tasks using the Insert Task page. If done correctly, you should see this lone task evicted.

You can check this by heading over to the View Tasks page and it should display the following (in resemblance):

No Image Found
View tasks page displaying a total of five tasks with the first one evicted

The task we created the first time is now evicted. This follows the LRU Cache principle of eviction which states that if the cache is at capacity, the least recently accessed item is evicted to make room for the new one.

Since the capacity is set to five, the sixth task is added by evicting the least recently accessed task. In this case, it happens to be the first one.

Try to delete any of your tasks in the Delete Task page:

No Image Found
Delete task page deletes the requested task

You should see a dropdown automatically populated with the IDs associated with your tasks. Selecting any of these IDs for deletion will remove them from the LRU cache.

You can view the updated list in the View Tasks page:

No Image Found
View tasks page showcasing four tasks with the fifth one deleted

Try updating a task in the Update Task page:

No Image Found
Update task page enabling the user to update any task by selecting any task from list of task IDs

Heading over to the View Tasks page should display the following:

No Image Found
View tasks page showcasing the updated task

The order of the LRU Cache changes with any insertion, deletion or updates. Whenever the retrieval or update process takes place, the key moves to the front of the cache.

This covers all areas of concern as it relates to the demo. Feel free to explore this web application.

Conclusion

In all, we investigated a good use-case for the LRU Cache data structure. We saw how a simple web application can incorporate the LRU Cache behind the scenes to serve end-users.

We did not require any external software to create this application. Everything was local including the database which was created using a simple JSON file.

Attached here is the code repository used in the demo. As always, I hope you found this article informative and look forward to more in the future.

Thank you!

No Name

Abdullah Muhammad

Blogger. Software Engineer. Designer.

Subscribe to the newsletter

Get new articles, code samples, and project updates delivered straight to your inbox.