Search code examples
javascriptcachingasync-awaitfetch-api

js async await fetch with cached approach


I have to add a list of data structures to a js object on an HTML page. I have a list of data structures on the server side each identified by a data-key. A js function loops the list of data-keys the function gets each structure one at a time from a async_cache using the corresponding data-key. There may be multiples of same data-keys in the list

  1. if the async_cache doesn't have the data-structure by that data-key it async-fetches and adds it to the cache.
  2. if the async_cache has the data-structure by that data-key it returns it right away.
  3. if the async_cache has already requested a fetch for a data-key and before the data arrives has another request for the same data-key it must not duplicate the request but wait until the data arrives.

I have constructed the following code so far (as an example for discussion). The actual environment can't be shared.

<?php 
    switch($_GET['--action'] ?? false){
        case 'data-structure':{
            $data_group = [
                'data-01' => ['a' => 'info-01', 'b' => 'extra-01'],
                'data-02' => ['a' => 'info-02', 'b' => 'extra-02'],
                'data-03' => ['a' => 'info-03', 'b' => 'extra-03'],
                'data-04' => ['a' => 'info-04', 'b' => 'extra-04'],
                'data-05' => ['a' => 'info-05', 'b' => 'extra-05'],
            ];
            if($data_struct = ($data_group[$_GET['key'] ?? false] ?? false)){
                \header('Content-Type: application/json');
                echo json_encode(['status'=> 'ok', 'data' => $data_struct], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
                exit();
            } else {
                http_response_code(404);
                echo json_encode('Not Found', JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
                exit();
            }
        } break;
    }
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta key="viewport" content="width=device-width, initial-scale=1.0">
    <title>JS Async Data Buffer Experiment</title>
</head>
<body>
    <div class="container">
        
    </div>
    <script>
        xui = {};
        xui.async_cache = {
            async async_get_await(url = '', options = {}){
                // Default options are marked with *
                const response = await fetch(
                    url, 
                    {
                        method: 'GET', // *GET, POST, PUT, DELETE, etc.
                        mode: options.mode || 'same-origin', // no-cors, *cors, same-origin
                        cache: options.cache || 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
                        credentials: options.credintials || 'same-origin', // include, *same-origin, omit
                        redirect: options.redirect || 'follow', // manual, *follow, error
                        referrerPolicy: options.referrerPolicy || 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
                    }
                );
                
                return response.json(); // parses JSON response into native JavaScript objects;
            },
            get(action, key){
                if(key in this){
                    return new Promise((resolve, reject) => {
                        resolve(this[key]);
                    });
                } else {
                    return this.async_get_await(
                        `?--action=${action}&key=${key}`,
                    ).then((r) => {
                        this[key] = r.data;
                        return this[key];
                    });
                }
            },
        };
        
        window.addEventListener('DOMContentLoaded', function(){
            
            var list = [
                'data-01',
                'data-01',
                'data-02',
                'data-01',
                'data-03',
                'data-02',
                'data-04',
                'data-02',
                'data-01',
            ];
            
            list.forEach((key) => {
                console.log({key});
                xui.async_cache.get('data-structure', key).then((data) => {
                    console.log(data);
                });
            });
        });
        
    </script>
</body>
</html>

Case 1 and 2 are taken care of but case 3 is where I am stuck. As you can see the code in the addEventListner executes in one shot before the data is received or event-loop kicks-in. So multiple requests go out for the same data-key.

I am looking for a minimalist approach. If there is a coding feature in JS library without using third party library - that will do as well.

I've also looked into the cache option on the Fetch-API but that is not a solution.

Thanks!


Solution

  • Thanks to @Bergi I finally figured it out! I rewrote the async_cache and went on simplifying ... Here is what I got (I've added a few test buttons as well):

    <?php 
        switch($_GET['--action'] ?? false){
            case 'data-structure':{
                $data_group = [
                    'data-01' => ['a' => 'info-01', 'b' => 'extra-01'],
                    'data-02' => ['a' => 'info-02', 'b' => 'extra-02'],
                    'data-03' => ['a' => 'info-03', 'b' => 'extra-03'],
                    'data-04' => ['a' => 'info-04', 'b' => 'extra-04'],
                    'data-05' => ['a' => 'info-05', 'b' => 'extra-05'],
                ];
                if($data_struct = ($data_group[$_GET['key'] ?? false] ?? false)){
                    \header('Content-Type: application/json');
                    echo json_encode(['status'=> 'ok', 'data' => $data_struct], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
                    exit();
                } else {
                    http_response_code(404);
                    echo json_encode('Not Found', JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
                    exit();
                }
            } break;
        }
    ?>
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta key="viewport" content="width=device-width, initial-scale=1.0">
        <title>JS Async Data Cache Example</title>
    </head>
    <body>
        <div class="container">
            <button type="button" onclick="run()">Run</button> <br/>
            <button type="button" onclick="delete xui.async_cache['data-01']">Delete 01</button> <br/>
            <button type="button" onclick="delete xui.async_cache['data-02']">Delete 02</button> <br/>
            <button type="button" onclick="delete xui.async_cache['data-03']">Delete 03</button> <br/>
            <button type="button" onclick="delete xui.async_cache['data-04']">Delete 04</button> <br/>
            Look at the console for responses and check the network tab as well!
        </div>
        <script>
            xui = {};
            xui.async_cache = {
                get(key){
                    return this[key] ?? (this[key] = (fetch(`?--action=data-structure&key=${key}`).then((response) => {
                        return response.json();
                    })));
                }
            };
    
            function run(){
                
                var list = [
                    'data-01',
                    'data-01',
                    'data-02',
                    'data-01',
                    'data-03',
                    'data-02',
                    'data-04',
                    'data-02',
                    'data-01',
                ];
                
                list.forEach((key) => {
                    console.log({key});
                    xui.async_cache.get(key).then((data) => {
                        console.log(data);
                    });
                });
                
            }
            
            window.addEventListener('DOMContentLoaded', function(){
                run();
            });
            
        </script>
    </body>
    </html>
    

    Looks like we can take advantage of the closure's data storage (or caching) inside the promise. And we don't need await or async, atleast for this rudimentary stage.