Showing posts with label Flask. Show all posts
Showing posts with label Flask. Show all posts

Monday, November 11, 2024

Cache in JavaScript: Service Workers

 

Caching is a technique for storing data in a temporary storage area called a cache. It aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. In the first article, I introduced the different possibilities for caching data in JavaScript and how to choose the appropriate caching possibility according to your needs. In a previous article, I introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. Then, I dedicated another article to using objects and data-* attributes to temporarily cache data in JavaScript. In this article, I will elucidate the use of service workers to cache static resources like HTML, CSS, and JavaScript files or API responses.


1. Service Workers API

This technique is used especially to cache static resources (HTML, CSS, etc.) or API responses. It helps enhance the responsiveness and performance of the web application. Please note that ‘service worker’ is a complex topic and it is impossible to cover all its features in this article. That’s why we focus only on the basic features to show how they cache resources and API responses.


The Service Worker API is a script that runs asynchronously and independently of the main browser thread.

- It is a kind of interface between the browser and the network.

- It is executed in the background.

- It handles tasks such as intercepting network requests and caching assets to allow the application to continue working even offline.


2. Tasks of Service Worker API

Let’s summarize the main tasks that service workers can perform when are ready to operate.


Interception of Network Requests: Service workers can intercept network requests and decide to handle them. Service workers can respond with cached resources or search data from the network according to some conditions like the network connection. If it is a poor connection, it responds to the client from cached resources, allowing fast loading and enhancing offline working. 


Background Sync: Service workers can synchronize data in the background, sending data to the server even if the app is not open. For example, they save user input while the connection is unavailable and send the data when the connection is back.


Caching: Service workers cache resources like HTML, CSS, JavaScript, and images. Thus the web app can be loaded faster and even continue working offline.


Push Notifications: Service workers handle push notifications so the web app sends messages to users even if the corresponding web page is closed.


3. Security Considerations

- Since service workers intercept network requests to make decisions, they must operate in a secure environment, which means they can only operate over HTTPS.

- Service workers run in a thread independent of the main thread, so they do not block the normal execution of the user interface.


4. Benefits of Service Workers

- Service workers improve the performance of web applications. Since they cache resources, they significantly reduce server calls, which reduces load times and strain on the server.

- Worker services allow offline access. In other words, users can interact with the application without an internet connection.

- Through push notifications, service workers keep users informed with regular updates.


5. Service Worker Lifecycle

The Service Worker lifecycle is event-driven and consists of several phases that describe how a Service Worker is installed, activated, and managed. The following table summarizes the key phases of a Service Worker's lifecycle. Note that a step can only be executed if the previous step has been completed successfully.

Service Worker Lifecycle

Phase

Definition

Code

Registration

A service worker must be registered by a page before it starts to work.

Method: ServiceWorkerContainer.register()

Download

A service worker is downloaded to the client.


Installation

- The browser installs the Service Worker.

- The install event is triggered.

- You can handle the install event to cache resources or complete an initial setup.

Event: install

Activation

- Once installed, the service worker is activated.

- Old caches are cleaned.

- The service worker gets ready to take control of the application.

Event: activate

Fetch

The service worker can intercept network requests using the fetch event and respond to them from the network or the cache.

Event: fetch


6. Example of Registering a Service Worker

Let’s take an example of how you can cache resources and API responses using the service worker API. For this, we need some back-end code to return data. A straightforward way to do this is using Python’s Flask framework. Additionally, we develop HTML, CSS, and JavaScript code for the front-end.


Note. We only write the relevant part here to explain how to use service workers to cache resources and intercept network requests. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/serviceworker-app-blog


Python Code

In the back-end code, we first define ‘employees_datas’ as a list containing a collection of employees, where each dictionary represents data about an employee and includes details such as its ID, first name, last name, and email.


We define some routes to enable the user to access web pages. For instance, to access the root URL of the application, the index function is called, and it renders the ‘index.html’ template file to display the content on the web page. 


We have also a route for ‘employee.html’ file and another one for ‘about.html’ file. Note that in the ‘employee’ function, the ‘render_template’ function renders the specified template file and passes the ‘employees_data’ variable to the template.


@app.route('/')

def index():

    # Render the index.html file

    return render_template('index.html')


@app.route('/employee')

def employee():

    return render_template('employee.html', employees_data=employees_data)


HTML Code

In the front-end code, we define a few HTML files, ‘index.html’ as the main web page, ‘employee.html’ to display some content sent from the back-end, and ‘about.html’. An additional file called ‘layout.html’ will be defined as a common template that will be extended by the other files.


This HTML template provides the structure for a basic web page that uses the Bootstrap library for styling, jQuery for JavaScript functionality, and placeholders for dynamic content through a template engine ‘Jinja’ of the Flask framework.


The {% block swjs %}{% endblock %} block is a placeholder for a service worker script that can be inserted by the template engine only in the ‘index.html’ template.


Inside the ‘navbar’, there are different navigation links to different parts of the website:

- ‘Home’ links to ‘/’, which consists of loading the ‘index.html’ template.

- ‘Employee’ links to ‘/employee’, which consists of loading the ‘employee.html’ template.

- ‘About’ links to ‘/about’, which consists of loading the ‘about.html’ template.


<div class="container"> holds the page’s main content and uses Bootstrap’s container class for responsive layout.

{% block content %}{% endblock %} serves as a placeholder for page-specific content that other templates can insert.


<head>

    ...

    {% block swjs %}{% endblock %}

</head>

<body>

    <nav class="navbar navbar-expand-lg navbar-light bg-primary">

        <a class="navbar-brand" href="/">SW Demo</a>

        <div class="collapse navbar-collapse">

            <ul class="navbar-nav mr-auto">

                <li class="nav-item">

                    <a class="nav-link" href="/">Home</a>

                </li>

                <li class="nav-item">

                    <a class="nav-link" href="/employee">Employee</a>

                </li>

                <li class="nav-item">

                    <a class="nav-link" href="/about">About</a>

                </li>

            </ul>

        </div>

    </nav>


    <div class="container">

        {% block content %}{% endblock %}

    </div>

</body>

</html>


Within the ‘index.html’ file, we have the following code:


{% extends "layout.html" %}


{% block swjs %}

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

{% endblock %}


{% block content %}

    <h1>Welcome to the Home Page</h1>

    <p>This is the home page of the Service Worker API Demo</p>

{% endblock %}


Let’s explain the main parts of this code:


{% extends "layout.html" %} indicates that this template file extends the ‘layout.html’ file,  meaning it inherits the structure and styling defined in ‘layout.html’.


{% block swjs %} and {% endblock %} define a block named ‘swjs’ where you can insert content specific to JavaScript files. In this case, it includes a script tag that links to the ‘main.js’ file located in the /static/js/ directory.


{% block content %} and {% endblock %}: This defines a block named content where you can insert the main content of the page. In this case, there is an <h1> heading and a <p> paragraph welcoming users to the Home Page of the Service Worker API Demo.


The content of the employee template in the file ‘employee.html’ is suggested as follows:


{% extends "layout.html" %}


{% block content %}

    <h1>Employee List</h1>

    <p>This is the employee List</p>

    <table>

        <thead>

            <tr>

                <th>Firstname</th>

                <th>Lastname</th>

                <th>Email</th>

            </tr>

        </thead>

        <tbody id="myTable">

            {% for item in employees_data %}

                <tr>

                    <td>{{ item.first_name }}</td>

                    <td>{{ item.last_name }}</td>

                    <td>{{ item.email }}</td>

                </tr>

            {% endfor %}

        </tbody>

    </table>

{% endblock %}


The above code is a Jinja template that extends a layout defined in ‘layout.html’. Within the {% block content %}, it creates an Employee List page with a table displaying employee data. The table has columns for Firstname, Lastname, and Email. It then loops through the ‘employees_data’ list and populates the table rows with the corresponding data for each employee’s first name, last name, and email address.


JavaScript Code

In the JavaScript code, we show how to register and use a service worker to cache data. Two JavaScript files are defined. The ‘main.js’ file is defined to register a service worker and the ‘sw.js’ file is used to encapsulate the service worker's behavior.


In the ‘main.js’ file, the code waits for the document to be fully loaded. Once the document is ready, it calls the ‘registerServiceWorker()’ function.


$(document).ready(function(){

    registerServiceWorker();

});


function registerServiceWorker() {

    console.log('Register Service Worker ...')

    if ('serviceWorker' in navigator) {

        navigator.serviceWorker.register(href = 'sw.js')

        .then(function(registration) {

            console.log('Service Worker registered with scope:', registration.scope);

        })

        .catch(function(error) {

            console.log('Service Worker registration failed:', error);

        });

    }

}


Function ‘registerServiceWorker()’: This function checks if the browser supports service workers by checking if ‘serviceWorker’ is in ‘navigator’. If service workers are supported, it attempts to register a service worker by calling ‘navigator.serviceWorker.register('sw.js')’. If the registration is successful, it logs a message to the console with the service worker’s scope. Note that the scope is the URL path that determines which pages the service worker will control. If the registration fails, it logs an error message to the console.


In the ‘sw.js’ file, we define the main behaviors of the service worker. This consists of handling the ‘install’, ‘activate’, and ‘fetch’ events. Thus we can install and activate the service workers. The ‘fetch’ event handler will intercept network requests to respond to the client from the cache.


First, we need a few global variables like the cache name (‘cache_name’) and the URLs we want to cache (‘urls_to_cache’).


// Cache name

const cache_name = 'sw-1';


// Urls to cache

const urls_to_cache = [

    '/',

    '/employee',

    '/about',

    '/static/css/styles.css',

    '/static/js/main.js',

    '/static/js/employee.js',

];


We add an event listener for the install event in the service worker as follows:


self.addEventListener('install', function(event) {

    console.log('Installing ...');

    event.waitUntil(

        caches.open(cache_name).then(function(cache) {

            console.log('Caching data ...');

            return cache.addAll(urls_to_cache);

        })

    );

}); 


When the service worker is installed, it performs some actions.  It uses ‘event.waitUntil()’ to extend the installation process until the provided promise resolves successfully. This means the service worker will only be considered installed if everything within ‘waitUntil’ (in this case, the caching process) completes without errors. 


It opens a cache (or creates one if it doesn’t exist) with the name specified by ‘cache_name’. Once the cache is opened, it adds all the URLs listed in the ‘urls_to_cache’ array to the cache for offline access. Note that the ‘Self’ word inside the service worker script refers to the Service Worker itself.


The activate event listener triggers when the service worker becomes active. This usually happens after installation and when there are no more active clients using the previous version of the service worker. The following code snippet is a listener for the ‘activate’ event:


// Activate event: Clean up old caches

self.addEventListener('activate', event => {

    console.log('Activating ...');

    const cacheList = [cache_name];

    event.waitUntil(

        caches.keys().then(cacheNames => {

            return Promise.all(

                cacheNames.map(cName => {

                    if (!cacheList.includes(cName)) {

                        console.log('Deleting old cache:', cName);

                        return caches.delete(cName);

                    }

                })

            );

        })

    );

});


When a service worker is activated, it means it is ready to take control of the page and handle caching and other tasks. Let’s explain what the code does. This code defines an array ‘cacheList’ containing the value of ‘cache_name’. This array specifies the caches to keep. Only the caches in this array will be kept, the others will be deleted.


It uses ‘event.waitUntil(...)’ to delay the activation until all old caches are deleted. This ensures a clean start with only the necessary caches.  Inside ‘event.waitUntil(...)’, it calls caches.keys() to get a list of all the cache names. It then uses ‘Promise.all(...)’ to map over each cache name and check if it's in the ‘cacheList’. If a cache name is not in the ‘cacheList’, it logs the cache to be deleted to the console and deletes that cache using ‘caches.delete(cName)’.


We now develop a common event listener that intercepts the ‘fetch’ event, which is triggered whenever a network request is made.


self.addEventListener("fetch", (event) => {

    console.log('Fetching data ...');

    // Intercept the network request to handle it

    event.respondWith(

        (async () => {

        // Search the response first in a cache

        let cache = await caches.open(cache_name);

        let cachedResponse = await cache.match(event.request);

        console.log("cachedResponse:", cachedResponse)


        // Return a response if it is found in the cache

        if (cachedResponse) return cachedResponse;


        // If no response is already cached, use the network

        return fetch(event.request);

    })(),

    );

});


After logging 'Fetching data ...' to the console, we call the ‘event.respondWith()’ method to intercept the network request and handle it. Within the ‘respondWith()’ method, we first try to find a response in a cache by opening a cache with the name ‘cache_name’ and attempting to match the request with the cached response. If a cached response is found, we return the cached response. If no cached response is found, we make a network request using fetch (‘event.request’).


7. Execution of the Example

When you run this example on the server, you get the following UI:

Visit all the pages and then stop the server. You will see that the application continues to work offline and the pages are showing normally.

Wednesday, October 16, 2024

Cache in JavaScript: In-memory Caching

 


Caching is a technique for storing data in a temporary storage area called a cache. It aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. In the first article, I introduced the different possibilities for caching data in JavaScript and how to choose the appropriate caching possibility according to your needs. I also introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. So, I will dedicate this article to using objects and data-* attributes to temporarily cache data in JavaScript.


1. In-memory Caching

This technique is used to retain data for a short period of time, such as the result of API calls during a single session. The data will be removed from the cache after the page refreshes. Even though it only caches data for short-term caching while a user is on a page, this technique is still an efficient way to use resources and retrieve data quickly, instead of repeatedly searching for it on the server.


In this section, we will see through examples two approaches to temporarily caching data:

- JavaScript objects or variables

- data-* attributes


1.1. Using Objects or Variables for In-Memory Caching

JavaScript objects or variables to store data in memory are mechanisms suitable for short-term or temporary caching while a user is on a page. Let’s take an example of how you can cache the result of an API call using objects. For this, we need a back-end code to return data. An easy way to do this is to use Python’s Flask framework. Moreover, we develop HTML, CSS, and JavaScript code for the front-end.


Note. We only write the relevant part here to explain how to use the objects to cache data. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/inmemory-caching-app-blog


Python Code

In the back-end code, we first define ‘james_bond_films’ as a list containing a collection of dictionaries, where each dictionary represents a James Bond film and includes details such as its ID, title, release year, star, and director.


james_bond_films = [

    {"id": 10, "title": "No Time to Die", "year": 2021, "starring": "Daniel Craig", "directedBy": "Cary Joji Fukunaga"},

    {"id": 20, "title": "Die Another Day", "year": 2002, "starring": "Pierce Brosnan", "directedBy": "Lee Tamahori"},

    {"id": 30, "title": "The Living Daylights", "year": 1987, "starring": "Timothy Dalton", "directedBy": "John Glen"},

    {"id": 40, "title": "A View to a Kill", "year": 1985, "starring": " Roger Moore", "directedBy": "John Glen"},

    {"id": 50, "title": "Never Say Never Again", "year": 1983, "starring": "Sean Connery", "directedBy": "Irvin Kershner"},

]


In this Python code, we define two methods that we explain as follows:


Method ‘get_film_data’: To return these details in response to an AJAX POST request, we define a Flask route in ‘/load_film_data’. It fetches the JSON data from the request, extracts the film ID, and searches for the corresponding film data in the predefined ‘james_bond_films’ list using the ‘search’ method. Finally, it returns the film data as a JSON response.


Method ‘search’: The search method takes two parameters, ‘id’ and ‘ls’. It searches for a film in the ‘ls’ list whose ID matches the provided ‘id’. If a match is found, it returns the corresponding data of that film as a dictionary. If no match is found, it returns None.


# Helper function to search for an item in a list

def search(id, ls):

    result = [element for element in ls if element["id"] == id]

    if len(result) > 0:

        return result[0]

    return None



# Route to handle AJAX POST request

@app.route('/load_film_data', methods=['POST'])

def get_film_data():

    # Get JSON data from request

    data = request.get_json()


    film_id = data.get('filmId')

    film_data = search(int(film_id), james_bond_films)

    print("Found film data: ", film_data)


    # Send a JSON response back to the client

    return jsonify({

        'status': 'success',

        'film_data': film_data

    })


HTML Code

In the front-end code, we define an HTML file with some elements like a drop-down list containing the list of some James Bond movies and a table to display the details of each movie when the user selects it.


<body>

    <div class="container">

        <h1>List of James Bond Films</h1>

        <select id="selectFilm" class="select-jb-film" onchange="onChangeJBFilm()" size="1">

            <option value="10">No Time to Die (2021)</option>

            <option value="20">Die Another Day (2002)</option>

            <option value="30">The Living Daylights (1987)</option>

            <option value="40">A View to a Kill (1985)</option>

            <option value="50">Never Say Never Again (1983)</option>

        </select>


        <br><br><br><br>

        <div id="film_detail">

            <table id="film_table">

                <thead>

                    <tr>

                        <th>Film Title</th>

                        <th>Year</th>

                        <th>Starring</th>

                        <th>Directed By</th>

                    </tr>

                </thead>

                <tbody>

                </tbody>

            </table>

        </div>

    </div>

</body>


JavaScript Code

In the JavaScript code, we implement the code for ‘onChangeJBFilm()’ and other behaviors to demonstrate the use of JavaScript objects to cache data. The code handles loading, storing, and displaying some data from the James Bond movie. In this section, we explain the main functions of the code.


We add a handler to the ‘DOMContentLoaded’ event to ensure that the JavaScript program starts once the HTML page is fully loaded and the elements are created. In this handler, we call the ‘onChangeJBFilm()’ function.


We define two global variables, the ‘cache’ object which we need to cache the data, and ‘apiUrl’ to get the data from the back-end.


// An object to store cached data

const cache = {};


// Url to get data from back-end

const apiUrl = "/load_film_data";


document.addEventListener('DOMContentLoaded', function() {

    onChangeJBFilm();

}, false);


Function ‘onChangeJBFilm()’: This function is called when the selected film is changed. This function is declared as ‘async’, meaning it will return a promise and can use the await keyword inside it. In the first line, it retrieves the current value of the drop-down, which is the selected movie’s ID.


The second statement line calls the ‘fetchFilmDataFromUrl()’ function with ‘apiUrl’ and ‘filmId’ as arguments. It waits for the function to complete and assigns the returned movie data to the ‘film_data’ variable. After logging the fetched film data into the console, the function called the ‘showMovieData()’ creates a new row in the ‘film_table’ table and displays the movie data.


async function onChangeJBFilm(){

    let filmId = document.getElementById("selectFilm").value;

    let film_data = await fetchFilmDataFromUrl(apiUrl, filmId);


    // Log the found data

    console.log("Film data: ", film_data);


    // Call the function with the URL and Id

    showMovieData(film_data);

}


Function ‘fetchFilmDataInMemCache()’: This function is defined to retrieve movie data from the cache. Otherwise, it fetches movie data from the back-end. The first statements of the function allow to retrieve the currently selected option from the select element. 


The function takes two arguments, ‘url’ as the endpoint where the data will be fetched from, and ‘filmId’, which is the ID of the specific film for which data is requested. The function creates a cache key by concatenating the ‘url’ and ‘filmId’. This uniquely identifies the cached data for that particular movie request.


The function checks if the data for the given ‘cacheKey’ is already stored in the cache. If so, the data will be returned immediately.


If the data is not cached, we need to make an API call. For that, the function creates a ‘dataToSend’ object containing the ‘filmId’ ID. It sends a POST request to the specified ‘url’ with ‘dataToSend’ as the request body and waits for a response. 


The function throws an error if the response is not OK. Otherwise, the response is parsed in JSON format. The retrieved data is stored in the ‘cache’ object using its ‘cacheKey’ key. Finally, the function returns the retrieved data.


// An async function that fetches data from a URL or in memory cache

async function fetchFilmDataInMemCache(url, filmId) {


    //Define cache key as the combination of url and the film ID

    let cacheKey = url+filmId;


    // Check if data is already in the cache to return it

    if (cache[cacheKey]) {

        console.log('Returning cached data');

        return cache[cacheKey];

    }


    console.log("Fetching data...");

    let dataToSend = {filmId: filmId}

    const response = await fetch(url,

        {

            method: 'POST',

            headers: {

                'Content-Type': 'application/json'

            },

            body: JSON.stringify(dataToSend)

        }

    );


    // Await the fetch request & throw an error if status is not ok

    if (!response.ok) {

        throw new Error(`HTTP error! status: ${response.status}`);

    }


    // Await the parsing of the response as JSON

    const data = await response.json();


    // Store the fetched data in cache

    cache[cacheKey] = data.film_data;

    return data.film_data;

}


Function ‘showMovieData(film_data)’: This function dynamically creates a row in the movie details table with data such as the movie title, release year, lead role, and director. Let’s note that we create only one row every time we insert movie data. So, we clear the table’s body before adding the new row. 


function showMovieData(film_data){

    let table = document.getElementById("film_table");

    let tBody = table.getElementsByTagName('tbody')[0];

    // Clear the tbody contents to create a new row

    tBody.innerHTML = "";

    let film_title = film_data["title"];


    //Add a new row

    let tr = document.createElement('tr');

    addCell(tr, film_title);

    addCell(tr, film_data["year"]);

    addCell(tr, film_data["starring"]);

    addCell(tr, film_data["directedBy"]);

    tBody.appendChild(tr);

}


Function ‘addCell(tr, item)’: This function adds a cell with a given content to a table row.


function addCell(tr, item){

    var td = document.createElement('td');

    var content = document.createTextNode(item);

    td.appendChild(content);

    tr.appendChild(td);

}


If you execute this code and open the browser, you find the following page:



1.2. Using Element Data Attributes for In-Memory Caching

In JavaScript, you can use data-* attributes to cache data on DOM elements. The role of data-* attributes is to store any additional information you need it in your code. The content of data-* attributes must be valid HTML5.

You can add data-* attributes to HTML elements directly, or you can add and access them in JavaScript. We briefly discuss the two mechanisms for using the defined data-* attributes. We then present an example of caching data using data-* attributes in JavaScript.


Adding data-* attributes to HTML elements

In your HTML, you can add use data-* attribute like that:

<div id="myFilm" data-film-id="10" data-film-title="Die another day"></div>


Accessing data-* attributes in JavaScript

In JavaScript, you can access data-* attributes using the dataset property of the DOM element. The dataset property is a read-write object that contains all the data-* attributes of an element.


// Select the element

let element = document.getElementById('myFilm');


// Read data attributes using the dataset property

let filmId = element.dataset.filmId;


// Write data in the element's data attributes

element.dataset.filmId = "Never Say Never Again";


We can also cache JavaScript objects using data-* attributes, but we must to convert them first to a string.


Example of Using Attribute Data of an Element

We keep the same example above, except we remove the ‘fetchFilmDataInMemCache()’ function and define a new function called ‘fetchFilmDataElemCache()’ to cache the data using the data-* attribute.


Function ‘fetchFilmDataInMemCache()’: This function is defined as an asynchronous function that takes a ‘url’ and a ‘filmId’ as parameters. In the first two statements, it selects an HTML element using its ‘selectFilm’ ID and retrieves the currently selected option from this element.

It checks if the selected option already has movie data stored in its data-* attribute. If so, it logs a message and returns the cached data. Here, we use ‘JSON.parse’ to convert the cached data, which is a string, into a JavaScript object.

If the data is not cached, it sends a POST request to the specified URL with the ‘filmId’ in the request body. It then waits for the response, checks for errors parses the response as JSON, and stores the fetched data in the option’s data-* attribute for future use. Here we need to convert the ‘data.film_data’ object to a string that we can store in the data-* attribute.


// An async function that fetches data from a URL or an element cache

async function fetchFilmDataElemCache(url, filmId) {


    // Select the element

    let element = document.getElementById('selectFilm');


    //Get the selected option

    var option = element.options[element.selectedIndex];


    // Access data attributes using the dataset property

    let filmData = option.dataset.filmData;


    // Check if data is already in cache to return it

    if (filmData) {

        console.log('Returning cached data');

        return JSON.parse(filmData);

    }


    console.log("Fetching data...");

    let dataToSend = {filmId: filmId}

    const response = await fetch(url,

        {

            method: 'POST',

            headers: {

                'Content-Type': 'application/json'

            },

            body: JSON.stringify(dataToSend)

        }

    );


    // Await the fetch request & throw an error if status is not ok

    if (!response.ok) {

        throw new Error(`HTTP error! status: ${response.status}`);

    }


    // Await the parsing of the response as JSON

    const data = await response.json();


    // Store the fetched data in the cache

    option.dataset.filmData = JSON.stringify(data.film_data);

    return data.film_data;

}




Blog Posts

Enhancing Performance of Java-Web Applications

Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and...